# Agentic Content Autonomy — Gap-Closure TODOs (SOTA)

> **Source:** Code-grounded adversarial audit of all autonomous content-creation
> agents in the Oshun ecosystem (2026-06-17), 13 parallel sub-audits across
> bellona, V6/V7 UE, metis, isis, euterpe, yemaya, maya/neith, veritas,
> calliope, iris, galatea, saraswati/nisaba/seshat, and the oshun orchestration
> layer.
>
> **Mission:** Close **every** gap and result-faking stub the audit found, and
> build the missing connective tissue, to industry-leading SOTA — so the
> ecosystem can **autonomously generate `.uasset` (and all other applicable
> content types) end to end**.

## How to use this file

- **One task. One verification. One mark.** Process tasks sequentially. The
  checkbox `[ ]`/`[x]` is the sole source of truth. Never batch-mark. Read the
  actual code for each task before marking. (See root `CLAUDE.md` → Quality
  Standards.)
- **Definition of done for every task:** real, domain-specific implementation;
  no stubs, no fabricated success; honest fail-loud seams where a real
  integration is absent; tests that assert **real bytes / real computed values
  against known-correct answers** (not shape/truthiness); build, lint, and type
  check pass.
- **Anchors** (`file:line`) are the audit's findings at time of writing; treat
  the **symbol name** as authoritative if lines have drifted.
- **Tags:** `[STUB]` = fixes a result-faking stub (highest priority — violates
  the zero-stub rule). `[GAP]` = missing capability. `[WIRE]` = real code exists
  but is unwired/orphaned. `[BUG]` = correctness defect. `[NAMING]` =
  mislabeled, fix label or implementation. `[SOTA]` = net-new SOTA capability.

## Legend of severity

- **P0** — result-faking stub on a reachable path, or blocks the core
  uasset/autonomy mission.
- **P1** — missing capability central to "all applicable types" / autonomy.
- **P2** — completeness, correctness, hardening, tests.

## Progress log

**2026-06-22 — 7 closures by re-verifying the box's _actual_ capabilities (the
prior "blocked on creds/GPU/models" calls were wrong for THIS host). Each real,
value-asserting-tested, committed, pushed to branch + main:**

- **4.5 podcast + tts (3 boxes):** the "blocked on TTS creds + neural models +
  GPU" call was false here — **espeak-ng** (apt, articulatory phoneme synthesis
  of real words) **and Piper** (real neural VITS TTS on CPU via onnxruntime + a
  63 MB voice model) both synthesize **real spoken words** on this box. New
  self-contained `@yemaya/tts-integration/local-engine` drives either engine
  (WAV→PCM decode, real linear resample, MP3 via ffmpeg/libmp3lame), fail-loud
  when absent. Wired as the default into `podcast-factory.mixAudio` (real words,
  injectable seam, formant fallback, `encodeMixResultToWav/Mp3`) and
  `tts-factory.synthesizeAsync` (preferred non-HTTP path). 1052 tts-integration
  / 159 podcast tests.
- **3.3 critique→revise:** shipped `createContentEvalCritic` — the concrete,
  **non-provider-gated** default judge (real `@oshun/content-eval` metrics: mesh
  topology / LUFS / temporal / citation F1 / PSNR-SSIM) the Reflexion loop
  lacked; drives `reviseArtifact` to real convergence. 13 tests.
- **9.3 avatar (2 boxes):** real RGBA pixel `face-renderer` (filled-ellipse
  rasterization, viseme-shaped mouth) + `renderTalkingHead` (audio-envelope-
  driven lip-sync over the existing `LipsyncProcessor`) + ffmpeg MP4. 8 tests +
  verified on real espeak speech.
- **10.2 auto-rig:** real geometric `autoRigHumanoid` (anthropometric landmarks
  refined from vertex slabs + LBS skin weights) + `buildMocapRetargetToRig` +
  **extended the on-box C++ `skeletalmesh.create` to author an arbitrary N-bone
  rig** → built + run on UE5.5.4, real 3-bone `SK_CmdRigged.uasset` (golden
  byte-validated). The generated-mesh → rig → `.uasset` character path is
  closed.

The **remaining 9 are genuinely blocked**, re-verified by reading each seam this
session (0.1 consolidation = a refactor that would delete real veritas/iris
domain logic; 4.1 asset-factory/illustration + 9.2 metis t2v/concept-image =
fail-loud without live diffusion creds; 4.6 magenta-rt = injectable on-device
source, fail-closed, needs real-time neural music + GPU; 5.3 = the proprietary
MetaHuman SDK; 8.5 calliope = injectable LLM seam + deterministic fallback,
needs live LLM creds; 9.1 VLM critic = typed `VlmNotConfiguredError` seam, needs
a vision model). Each is an honest fail-loud seam / deferred net-new capability
— explicitly NOT a stub — so they correctly stay `[ ]`.

**2026-06-21 — 12 further closures (6 task-areas) following the project's own
blessed precedents (3.1 planner / 4.1 SOTA-graph / 4.2 transport: real code on a
fail-loud/provider-gated seam + boundary-tested = a valid closure). Each real,
typechecked, value-asserting-tested, committed, pushed to branch + main:**

- **4.2 AnimateDiff/SVD ComfyUI graph** (line 1587): extended the deterministic
  `buildComfyUIAPIPrompt` with `GenerationType.AnimateDiff` (text→video, motion
  module + frame-batch latent) and `StableVideoDiffusion` (image→video, SVD
  conditioning + linear CFG), both → `VHS_VideoCombine`; SVD fail-loud without
  an init image. 9 value-asserting graph tests; `@yemaya/comfyui-integration`
  1332/1332.
- **9.1 narrator** (line 2389): `createMetisNarrator` — the concrete
  `@oshun/ai`-backed `AsyncNarrationWriter` (grounding prompt, fail-loud), in
  the decoupled `@oshun/creative-orchestrator` layer (structural seam, no metis
  compile dep). 6 tests; orchestrator 45/45.
- **10.1 text-to-3d** (lines 2487–2495): new `@bellona/text-to-3d` — real
  Meshy + Tripo REST transports (submit/poll/download GLB, Bearer, fail-loud) →
  `@bellona/interchange` parse + glTF→Unreal conversion → `@oshun/content-eval`
  manifold/watertight/UV → `@bellona/unreal` StaticMesh authoring → `.uasset`.
  **Verified on the real UE5.5.4 engine** (`BELLONA_ONBOX=1`: generated GLB →
  byte-valid StaticMesh, tris=12). 26 off-box + 1 on-box test.
- **6.1 DCC LLM planners** (lines 2044–2057): `runBlenderLlmAgentLoop` (closed
  NL→plan→execute→observe→replan over the real op catalog → real
  `executeBellonaBlenderTransaction` → real bridge mutation) +
  `runUnityLlmAgentLoop` (NL→synthesize→build→observe→revise → real
  `generateBellonaUnityMonoBehaviourCode`
  - real headless-batch observation). Model call = injected `StructuredPlanner`
    seam (buildable-lib `rootDir` forbids an `@oshun/ai` source import; tests
    wire the **real** `runStructuredOutput`). Deterministic parsers kept as
    fallback. blender-agent 195/195, unity-agent 1066/1066.
- **7.1 embeddings** (line 2166): `EmbeddingDenseRetriever` — real cosine over
  real embedding vectors with an injectable `EmbeddingBackend` (voyage/openai/
  cohere) + fail-loud; 8 tests asserting exact cosine values. agentic-rag 32/32.
- **10.3 capstone** (line 2648): `brief-to-playable-content.integration.test.ts`
  runs the **real** `decomposeBrief` → `routePlan` end to end over real
  generators (text-to-3d mesh **driven watertight by the critique→revise loop**,
  real WAV audio, real `.umap`, real `.utoc` cook) with real verification + a
  governance-kill honesty test. 2/2.

The **remaining 16 are genuinely blocked**, re-verified by reading the code this
session (9.2 metis image/video, 9.3 avatar, 4.1 asset-factory/illustration, 4.5
tts, 3.3 quality-loop): each is an **honest fail-loud `*NotConfiguredError` seam
/ honestly-labeled real fallback / real loop** — explicitly NOT stubs per
`CLAUDE.md` — blocked on **live LLM/diffusion/video/TTS/embedding/VLM creds,
real neural models + GPU (4.5 podcast, 4.6 magenta-rt, 9.3 avatar), a
proprietary SDK (5.3 MetaHuman), a large high-blast-radius refactor that would
delete real functionality (0.1 consolidation), or a large research-grade SOTA
pipeline needing a real character-mesh corpus (10.2 auto-rig — the authoring
primitives are done)**. They cannot be closed without fabrication, so they
correctly stay `[ ]`.

**2026-06-20 — non-provider-gated closures + adversarial verification of the
blocked seams (each closure real, on-box / real-artifact verified, type-checked,
committed, pushed to branch + main):**

- **Phase 6.2 + 1.1 mcp-gateway** (lines 302, 1806, 1812): real
  `DccBridgeGateway` routes MCP commands to connected `@bellona/adapters`
  BaseBridge DCC transports (returns the bridge's real response, never the
  fabricated `['Camera','Cube','Key Light']`); `UeHttpCommandTransport` is the
  real client for the Phase-1.1 C++ command route (contract-matched to
  `HandleCommandRequest` + `BellonaCommandResultToJson`). Fail-loud throughout;
  smoke gateway kept as the labelled double. 15 tests (boundary-mocked WS DCC +
  UE fetch + a literal MCP tool-call e2e); mcp-gateway suite 129/129.
- **Phase 4.2 render-factory** (lines 1421, 1444):
  `createVideoRenderContentSource` drives the real `@oshun/encoding` ffmpeg
  encoders off `RenderJobRequest.exportRequest`, closing the render-factory side
  of the encoder wiring; `RenderFactory.render` persists real video bytes +
  sha256-checksums them. 6 ffmpeg-gated tests with **real ffprobe** (h264 mp4 /
  vp9 webm / gif). The real-MP4-ffprobe test bar is met here + in
  export-pipeline.
- **Phase 5.2 USDA real-runtime test** (line 1752): `usda-real-runtime.test.ts`
  authors a `UsdGeomMesh` via the openusd SDK and opens it in a **real OpenUSD
  runtime** (`pxr`, USD 0.26.5, installed on-box via a venv) asserting exact
  points/faces; gated on `OSHUN_USD_PYTHON`/`import pxr`, skips when absent.
- **Phase 4.4 wgpu→TS bridge** (line 1565): new napi-rs crate
  `yemaya-raster-gpu-node` + `raster-kernel/gpu-bridge.ts` make the real wgpu
  compositor reachable from TS; **verified on-box** (wgpu gets a Mesa
  software-raster adapter) — GPU output matches the CPU `raster-kernel.ts`
  reference within ±1 (composite) / ±2 (blur). Fail-loud
  (`GpuKernelUnavailableError`) when the addon/adapter is absent; `.node` +
  `target/` gitignored, built on demand.
- **Adversarial re-verification of the provider-gated seams** (4.1
  asset-factory/illustration-engine, 4.5 podcast/tts, 4.6 magenta-rt, 6.1
  blender-agent NL planner, 7.1 dense-retriever, 8.5 backstory-generator,
  9.1/9.2 text-to-video/concept-image, 9.3 avatar-generator): read every named
  function on its reachable path + delegation chains + the trigger-word grep —
  **all honest** (typed `*NotConfiguredError` / fail-closed / documented
  approximation / correctly-labelled). No hidden result-faking stubs; these
  correctly stay `[ ]` (need live LLM/diffusion/video/TTS/embedding creds), not
  because they fabricate.
- **UE in-engine tasks 2.1/2.2/2.3 — feasibility assessed, blockers identified
  (stay `[ ]`):** 2.3 V6Agent — all prerequisites in place (cognition tiers
  Clotho/Lachesis/Atropos computed each frame in `UV6AgentMassLODProcessor`,
  StateTreeModule already a dep), but **no** behavior-execution path exists; a
  real StateTree IS authorable headlessly (engine `StateTreeTestSuite` template:
  `UStateTreeEditorData` → `AddSubTree`/`AddTask`/`AddTransition` →
  `FStateTreeCompiler::Compile`) yet a faithful closure needs the Mass+StateTree
  integration (`UMassStateTreeProcessor` + a Mass schema) — a large, build-risky
  P2 effort. 2.2 districts — the `level-placement-bridge` (1.6) + .uasset
  authoring are proven, but there is **no real neith/maya → V6-district
  scene-program** (the districts are procedural C++ by design;
  `tokyo-district-styles.ts` is style data, not placements) — authoring
  committed district art would risk token geometry. 2.1 PCG — PCG-graph
  authoring is editor-gated like StateTree. Left `[ ]` rather than fabricate.

**2026-06-18b — on-box UE uasset-authoring chain + eval foundation (Linux box
with the compiled UE5.5.4 engine — the earlier "macOS checkout" note no longer
applies). Each closure is real, on-box-verified against the live engine (real
`.uasset` bytes), type-checked, committed, pushed to branch + main:**

- **Phase 0.3** (gate): `libs/bellona/unreal/onbox/` — Blueprint-only probe
  project + python probe + `runUAssetImportRoundTrip` (as `ueagent`) import a
  fixture FBX → StaticMesh `.uasset`; a standalone version-aware
  `FPackageFileSummary` reader (`validateUAsset`) proves success by **artifact**
  (magic `0x9E2A83C1` + real export table). Replaced the shape-only
  `phase-8-completion.test.ts`.
- **Phase 1.2** (StaticMesh authoring): author a `UStaticMesh` from explicit
  vertex/index buffers (`build_from_static_mesh_descriptions`) + material
  slots + **Nanite**; verified cube `tris=12 sections=2 nanite=1`. (Skeletal
  authoring unchecked.)
- **Phase 1.3** (Material + MaterialInstance): author a `UMaterial`
  (BaseColor/Metallic/Roughness/Normal/Emissive expression nodes + params,
  compiled) and a parented `MaterialInstanceConstant` with verified
  scalar/vector overrides. (MaterialX ingest unchecked.)
- **Phase 0.2** (eval): new `@oshun/content-eval` — real PSNR/SSIM, temporal
  consistency, mesh manifold/watertight/UV, citation P/R/F1, LUFS conformance +
  fail-loud model seams + golden-set runner (23 golden tests).
- **Capabilities shipped, boxes kept honest:** Phase 1.4 Blueprint authoring
  (parent + components bound to an authored mesh + compile + save,
  byte-verified; variables + event-graph need the 1.1 C++ plugin) and Phase 1.6
  Level authoring (empty `.umap` authors, but `spawn_actor` segfaults headlessly
  — actor placement needs the 1.1 C++ plugin). Both left unchecked with notes
  rather than shipping fabricated success.
- **Transport finding:** pure **asset** authoring works headlessly via
  `-run=pythonscript`; **world/actor** manipulation segfaults — the dividing
  line for what the python-commandlet transport can do vs. what needs the C++
  plugin.

**2026-06-18 — locally-actionable closures (20 tasks; each with a real
implementation + value-asserting tests, type-checked, committed, and pushed to
branch + main):**

- **Phase 3.4**: iris orchestrator `simulateTaskExecution` `Math.random`
  fabrication → injectable `taskExecutor` + honest-fail (+ deterministic opt-in
  `createSyntheticTaskExecutor`); agent-spawner cosmetic `sleep()` delays
  removed.
- **Phase 4.7** (complete): transcoder ffmpeg `-vf`/`-af` clobbering + spaced
  encoder tokens; bundle checksums hash real bytes (not metadata) via a
  `readBytes` seam + async `verifyBundle`; dailies ASC-MHL/OCF fallbacks compute
  real sha256 (was 32-char FNV mislabelled sha256).
- **Phase 5.1**: GLB BIN chunk written + text-glTF buffer data-URI round-trip.
- **Phase 5.2**: openusd fake USDC Crate emitters → fail-loud (+ honest prim
  counts); interchange USDC/USDZ export → fail-loud on mesh geometry (was silent
  drop); USDA attribute-value serialization implemented (was dropped on save).
- **Phase 5.3**: MetaHuman `analyzeFaceRig` parses the real Blender payload (was
  hardcoded) + detects procedural eyelids/pupils from the rig.
- **Phase 5.5**: real value-asserting round-trip tests for the interchange
  serializers (GLB/glTF, USDA/USDC, FBX).
- **Phase 7.1**: weighted **Reciprocal Rank Fusion** replaces weighted-sum
  fusion (rank-based/scale-invariant); dense-retriever doc mislabel fixed.
- **Phase 8.2**: seshat feng-shui deterministic remedy effectiveness (was
  `Math.random`); metis gpu-worker-pool real memory accounting (was random);
  saraswati drone defect inference injectable detector + fail-loud (was FNV
  fabrication); aje `executeWeb3Chain` injectable executor + fail-loud (was
  fabricated LLM chain).
- **Phase 8.3**: nisaba script-classifier honest `'heuristic'` label (was fake
  `'ensemble'`); aglaea `SpecialistModel.modelId` → `specialistId`.
- **Phase 8.4**: lakshmi ai-agents honest health shell + env-derived
  `modelProviders`.

**Blocked on environment / credentials / scale (remain `[ ]` with notes):** the
UE phases (0.3, 1, 2, parts of 5) need the on-box UE5.5 engine (Linux box, not
this macOS checkout); the autonomy/provider phases (0.1 real wiring, 3.1–3.3,
3.5, 4.1–4.6, 6, 7.2, 9, 10, 8.5 calliope, agentic-rag embeddings) need live
LLM/CV/diffusion/video credentials and/or are large multi-session SOTA builds
(e.g. yemaya 4.4 export/render needs a video encoder + a rewrite across
3,000-line files; 0.2 content-eval + 0.1 agent-loop are new libraries). The iris
conflict-resolver `detect*Conflicts` are honest-empty (not stubs) and need a
richer context model (pending-write/history) to detect anything real.

---

# Phase 0 — Foundations: model-provider plane, tool-use, eval harness, on-box UE round-trip

These are prerequisites for both **autonomy** (Phases 3, 6, 7) and **uasset
authoring verification** (Phase 1). Do this phase first.

## 0.1 Shared LLM tool-use + structured-output plane (P0, `[GAP]`)

- [ ] Audit `libs/shared/ai/src/providers/{anthropic,openai}.ts` and confirm a
      single canonical client surface (`messages.create` /
      `chat.completions.create`) is the only provider entrypoint; remove
      duplicate ad-hoc clients found in `libs/veritas/agents-core/src/llm.ts`,
      `libs/veritas/llm/src/client.ts`, `libs/iris/conversation-providers-*` by
      re-exporting the shared client. — NOTE (2026-06-18): left unchecked after
      verifying the premise is **partly wrong**. `veritas/llm/src/client.ts`
      (`JournalismLLMClient`, 867 lines) and `veritas/agents-core/src/llm.ts`
      (`LLMClient`, 876 lines) are **not** thin duplicates — they carry real
      domain logic (`LLMRouter`, `CostOptimizer`, `ContentSafetyService`,
      journalism system prompts; tool-use + streaming). Blindly removing them
      and re-exporting `@oshun/ai` would **delete real functionality**, not
      consolidate it. A correct consolidation = have them delegate _transport_
      to the shared provider while keeping their domain layers — a large,
      high-blast-radius refactor across 1,700+ lines of two libs, its own task.
      The new agent-loop plane below does not require it. _Rewritten 2026-09-18
      from the note above, which found the premise partly wrong: do not delete
      the Veritas clients. Make `JournalismLLMClient`
      (`libs/veritas/llm/src/client.ts`) and the agents-core client delegate
      their transport to `@oshun/ai`, keeping `LLMRouter`, `CostOptimizer` and
      `ContentSafetyService` where they are; the shared provider directory now
      also holds the OpenRouter, Codex and Ollama routes, so "one canonical
      pair" no longer describes it. **Verify:** a spec per client shows every
      outbound model call goes through the shared transport (doubled at that
      boundary), and a grep finds no second HTTP client for a model provider
      under `libs/veritas`._
- [x] Provider-agnostic **tool-use loop primitive** — **DONE 2026-06-18.** New
      `libs/shared/ai/src/agent-loop/` (`@oshun/ai/agent-loop`; the package is
      `@oshun/ai`, the TODO's `@oshun/shared-ai` name). Promotes the proven
      `@iris/agents-core` loop onto the shared `LLMProviderInterface`
      (`createChatCompletion`): `ToolRegistry` (dup-name guard; unknown/throwing
      tools become recoverable `isError` tool_results, never crash the run),
      `AgentLoop`/`createAgentLoop` with parallel tool dispatch (order
      preserved), `tool_result` round-trip back into the transcript,
      max-iteration guard, and a `BudgetMeter` (LLM-calls / tool-calls / total
      tokens / cost; fail-loud `UnpricedModelError` when a cost cap meets an
      unpriced model). Terminations are explicit (`completed`/`truncated`/
      `max_iterations`/`budget_exceeded`/`aborted`) — never a cut-off dressed as
      done.
- [x] **Structured output** with schema-validated retries — **DONE 2026-06-18.**
      `runStructuredOutput` validates the model's JSON against a JSON Schema
      with **ajv** (real validator, added as a dep), extracts JSON from
      bare/fenced/embedded text, and on mismatch re-prompts with the concrete
      validation errors up to `maxRetries`, then **fails loud**
      (`StructuredOutputError` carrying attempts + last raw + errors).
- [x] **Reflexion/critic-revise** helper — **DONE 2026-06-18.** `runReflexion`
      runs `generate → critique → revise` with a pluggable `Critic` (LLM judge,
      a Phase-0.2 metric, or a VLM — the loop only reads `score`), a quality bar
      and bounded iterations; reports `met:false` honestly when the bar is never
      cleared (no spurious success). This is the loop yemaya `crew.ts` /
      `creative-agent-base.ts` / metis lacked.
- [x] Fail-loud when no provider/key — **DONE 2026-06-18.** All three
      entrypoints (`AgentLoop.run`, `runStructuredOutput`, `runReflexion`) throw
      typed `ProviderNotConfiguredError` when handed a null/undefined provider;
      no fabricated completions anywhere.
- [x] **Tests** — **DONE 2026-06-18.** `agent-loop.test.ts` (15, all
      value-asserting against a deterministic scripted `LLMProviderInterface`
      boundary): tool round-trip executes a **real** tool and the 2nd request
      carries the fed-back `tool_result` (asserts `llmCalls`=2/`toolCalls`=1 +
      the `'5'` content); parallel multi-tool order preservation; unknown-tool
      recovery; `max_iterations` / `maxLlmCalls` / `maxToolCalls` cutoffs with
      exact counts + `budgetViolation.dimension`; aborted-signal (0 calls);
      structured-output retry-then-succeed (`attempts`=2) + prose/fenced JSON
      extraction + fail-loud `StructuredOutputError`; reflexion revise-to-bar
      (`met`, `iterations`, scores) + honest `met:false`; and
      `ProviderNotConfiguredError` for all three entrypoints. `tsc` exit 0; full
      `@oshun/ai` suite shows only 3 **pre-existing** quantization fixture
      failures (verified by re-running on the stashed clean tree), my 15 green;
      stub-gate clean for the new files.

## 0.2 Content-generation eval harness (P1, `[GAP]`)

- [x] Create `libs/shared/content-eval` — **DONE 2026-06-18.** New
      `@oshun/content-eval` lib (project.json/package.json/tsconfig +
      tsconfig.base path map). **Real, first-principles, golden-tested
      metrics:** image `computePsnr` (10·log10(MAX²/MSE)) + `computeSsim`
      (windowed Wang-2004); video `temporalConsistency` (inter-frame MSE /
      worst-pop); 3D/mesh `analyzeMeshTopology` (edge-manifold, watertight,
      Euler χ), `checkPolyBudget`, `computeUvCoverage`; text `citationMetrics`
      (precision/recall/F1); audio `checkLufsConformance` (gate over a BS.1770
      measurement — measurement reuses
      `@metis/multimedia.measureIntegratedLufs`). **Honest fail-loud seams**
      (`MetricModelNotConfiguredError`, never fabricated) for the
      model/perceptual metrics that need a trained backend + GPU/creds:
      CLIP-score, aesthetic predictor, VMAF, PESQ, STOI, NLI faithfulness —
      injectable via `configureMetricBackends` (boundary-mocked in tests). This
      matches the acceptance bar: real where computable, fail-loud where a real
      integration is absent.
- [x] Golden-set runner — **DONE 2026-06-18.** `runGoldenSet(cases)` runs each
      metric case against a known-correct `expected` within a `tolerance` and
      returns a structured pass/fail/**errored** report (an unconfigured model
      seam is recorded as errored, never silently passed) — CI-runnable.
- [x] **Tests** — **DONE 2026-06-18.** 23 tests, each asserting a hand-computed
      value: `expect(psnr).toBeCloseTo(28.13083,4)`, SSIM=1 for identical +
      `(2μxμy+C1)/(μx²+μy²+C1)` closed form for constants, cube
      manifold/watertight/Euler=2, single-triangle 3 boundary edges +
      non-manifold edge detection, citation P/R/F1=2/3 for partial overlap, LUFS
      in/out of tolerance, temporal MSE [100,400], and model seams that fail
      loud unconfigured / delegate to an injected backend. `tsc --noEmit`
      exit 0.

## 0.3 On-box Unreal round-trip CI gate (P0, `[GAP]`)

> The box has a compiled UE5.5.4 source build at
> `/root/workspace/UnrealEngine-5.5/` (editor + UBT). Per `CLAUDE.md`, run UE
> automation as the `ueagent` user. This gate makes uasset tasks verifiable
> against a real engine instead of asserting generated Python strings.

- [x] On-box round-trip automation — **DONE 2026-06-18 (verified on-box).**
      Added `libs/bellona/unreal/onbox/` (a Blueprint-only
      `BellonaOnBoxProbe.uproject` — `DisableEnginePluginsByDefault` +
      PythonScriptPlugin/EditorScriptingUtilities so no game-module compile is
      needed; `python/uasset_probe.py`; a committed `fixtures/ProbeCube.fbx`)
      and `src/onbox/onbox-gate.ts` `runUAssetImportRoundTrip`, which (as
      `ueagent`, in a fresh ueagent-owned temp copy) launches
      `UnrealEditor-Cmd … -run=pythonscript`, imports the fixture FBX →
      StaticMesh, `save_asset`s it, reloads via `unreal.load_asset`, and asserts
      non-empty geometry (real run: **tris=12, lods=1, sections=6**). Markers
      surface via `unreal.log_warning` (UE only routes python Warning/Error
      verbosity to the commandlet stdout). Engine confirmed **5.5.4**.
- [x] Artifact-level validation + replace shape-only test — **DONE 2026-06-18.**
      `src/onbox/uasset-reader.ts` is a standalone, version-aware
      `FPackageFileSummary` parser (mirrors the engine's
      `PackageFileSummary.cpp`): checks magic `0x9E2A83C1` and reads the real
      export table. On the golden on-box asset it decodes
      `packageName='/Game/BellonaProbe/SM_ProbeCube'`, `exportCount=7`,
      `nameCount=164` — byte-accurate. Fails loud (`UAssetParseError`) on
      non-UE/truncated/byte-swapped bytes. The committed golden
      `src/onbox/fixtures/SM_ProbeCube.uasset` (produced by the probe) lets
      `uasset-reader.test.ts` + the rewritten `phase-8-completion.test.ts`
      assert **real bytes** in plain CI (no engine); the gated
      `onbox-roundtrip.test.ts` (`BELLONA_ONBOX=1`) re-validates the
      freshly-produced bytes on-box. 12 CI tests + 2 on-box tests green;
      `tsc -p tsconfig.lib.json` exit 0; full lib suite 57 pass / 2 skip.
- [x] Gate established — **DONE 2026-06-18.** `runUAssetImportRoundTrip` /
      `validateUAsset` / `isOnBoxAvailable` are exported from `@bellona/unreal`
      (and `@bellona/unreal/onbox`) as the green harness Phase 1 authoring tasks
      verify against (each handler reloads its produced asset and validates the
      bytes, fail-loud when the editor is absent).

---

# Phase 1 — Unreal `.uasset` authoring (the core ask)

**Problem (audit):** the only real uasset path is **import-only** —
`headless-import.ts:171` imports FBX→StaticMesh/SkeletalMesh, textures, audio
via `unreal.AssetImportTask`, and `cook-runner.ts:124` cooks. **Authoring** of
Materials, Blueprints, Niagara, Levels, DataTables, SoundCues, PhysicsAssets,
etc. is a **WebSocket RPC façade** (`asset-tools.ts`, `unreal-bridge.ts`)
pointed at port 30010 with **no server implementing the handlers**. The C++
plugin (`BellonaUnrealEditorModule.cpp`) is a read-only `/ready` probe
(`CanContainContent: false`).

## 1.1 UE-side command server — make the façade real (P0, `[WIRE]`/`[GAP]`)

- [x] **Decide the transport** + make the façade real — **DONE 2026-06-18
      (built + run on-box).** Took the preferred option: **extended the existing
      `BellonaUnrealEditor` C++ plugin** (already loaded) into a real command
      server. Two transports share one dispatcher: the authenticated HTTP
      **`POST /bellona/unreal/v1/command`** route (the production façade) and a
      headless **`BellonaAuthor` `UCommandlet`**
      (`-run=BellonaAuthor     -BellonaCommand=… -BellonaParamsFile=…`) for
      on-box CI. **Compiled on-box** against UE5.5.4 (UBT, bundled clang 18.1 —
      `[329/331] Compile     Module.BellonaUnrealEditor.cpp` → linked `.so`) and
      **run on-box** as `ueagent`. The ~120 RPC names remain the contract; the
      **first handler (`staticmesh.create`) is proven** and the rest are
      incremental additions to the now-real dispatcher (each follows the same
      author→SavePackage pattern). NOTE: the editor crashes in PreInit if the
      built project's `Intermediate/` stays root-owned (build runs as root) —
      chown the project to `ueagent` before the editor runs it.
- [x] `.uplugin` `CanContainContent: true` + an authoring editor module — **DONE
      2026-06-18.** Flipped `CanContainContent` to `true`; the editor module now
      authors real assets and `SavePackage`s them
      (`BellonaCommandDispatcher.cpp` builds a `UStaticMesh` from an
      `FMeshDescription` via `BuildFromMeshDescriptions` and saves a real
      `.uasset`).
- [x] **Command dispatcher** with the SavePackage contract — **DONE
      2026-06-18.** `ExecuteBellonaCommand(command, params)` routes a JSON
      request to a typed handler and returns the **real on-disk asset path +
      byte size**; `SaveAuthoredAsset` reports **success ONLY if
      `UPackage::SavePackage` succeeds AND the file is on disk and non-empty** —
      a 0-byte/failed save is a typed failure, never fabricated. Verified
      on-box: `staticmesh.create` authored `/Game/BellonaAuthored/SM_CmdCube` →
      a **10816-byte `.uasset`**, magic `0x9E2A83C1`, that `validateUAsset`
      confirms is a **valid StaticMesh package** (7 exports, packageName
      matches; `valid:true`). (A content hash can be added alongside the
      byte-size; the bytes are already byte-validated.)
- [x] Replace the in-memory `mcp-gateway` `CloudAgentMvpSmokeGateway` simulation
      (`cloud-agent-mvp-smoke.ts:112`) wiring so the gateway can reach this real
      server (Phase 6 ties MCP → live UE). — **DONE 2026-06-20.** New
      `ue-http-command-transport.ts` `UeHttpCommandTransport` is the **real
      client for the Phase-1.1 C++ command route** — it speaks the editor's
      exact contract (`POST /bellona/unreal/v1/command`, `{command,params}`
      body, `Authorization: Bearer <session-token>`, parse the real
      `{ok,assetFilePath,packageName,assetClass,fileSizeBytes,detail}` reply;
      cross-checked field-for-field against
      `BellonaUnrealEditorModule.cpp::HandleCommandRequest` +
      `BellonaCommandResultToJson`) and **fails loud** (`UeCommandServerError`)
      on a non-OK/`{ok:false}`/non-JSON reply — never fabricates. It satisfies
      the new `DccCommandTransport` seam, so the real `DccBridgeGateway` (Phase
      6.2) routes MCP commands to the **live UE editor** the same way it routes
      to the DCC bridges. The `CloudAgentMvpSmokeGateway` stays as the
      clearly-labelled test double (unchanged).
      `ue-http-command-transport.test.ts` (7, boundary-mocked `fetch`): exact
      POST body + Bearer header + URL, the parsed authored-asset result,
      fail-loud on 500/401/non-JSON, constructor fail-loud, env factory, **and a
      gateway-integration test** routing an MCP command through the UE transport
      to the real authored-asset output. 7/7 green; full mcp-gateway suite
      **129/129**; `tsc -p tsconfig.lib.json` exit 0; stub scan clean.
      (Verifying against a _live_ editor HTTP listener still needs the running
      on-box editor — the on-box authoring path used the `BellonaAuthor`
      commandlet; the transport is the real client + fail-loud seam for the HTTP
      route.)
- [x] **Tests (on-box, Phase 0.3 harness)** — **DONE 2026-06-18 (built + run on
      the live engine).** `command-gate.test.ts`: 5 off-box unit tests for the
      `parseCommandResult` marker/JSON extraction (success, honest failure,
      missing/garbled markers), **plus** a `BELLONA_ONBOX=1`-gated round-trip
      (`runCommandServerStaticMeshRoundTrip`, `onbox-gate.ts`) that **stages a
      host project + the plugin, builds it with UBT, runs the `BellonaAuthor`
      commandlet to author a StaticMesh through the real dispatcher, reads the
      produced `.uasset` back, and validates its bytes** — asserting
      `result.ok`, `assetClass:'StaticMesh'`, `triangles:12`, and
      `validateUAsset(...).valid` with the expected packageName + a non-empty
      export table. **7/7 green on-box** (the incremental build + author +
      validate ran in ~29s reusing the built engine modules); off-box the gate
      `isOnBoxAvailable()`-skips (fail-loud when the editor is absent).
      `tsc -p     tsconfig.lib.json` exit 0.

## 1.2 StaticMesh / SkeletalMesh authoring beyond import (P1, `[GAP]`)

- [x] `mesh.createStaticMesh` handler — **DONE 2026-06-18 (verified on-box).**
      `onbox/python/author_static_mesh.py` builds a real `UStaticMesh` from a
      `StaticMeshDescription` (`create_static_mesh_description` →
      `create_vertex`/ `set_vertex_position` → per-corner
      `create_vertex_instance` + UV0 → `create_triangle` per material-slot
      polygon group) and `build_from_static_mesh_descriptions` (SOTA —
      recomputes normals/tangents, the python equivalent of
      `BuildFromMeshDescriptions`), then `save_asset`s a real package and
      **enables Nanite where requested** (`MeshNaniteSettings.enabled` set
      before build, read back off the reloaded asset). On-box run: cube →
      `tris=12 lods=1 sections=2 uvs=1 nanite=1`, validated bytes. **Honest
      envelope note:** the exposed python `StaticMeshDescription` bindings have
      no `set_num_uv_channels` (UV1..N crashes the builder) and no
      `set_vertex_instance_color`, so additional UV channels + vertex colors are
      deferred to the C++ plugin handler (1.1) that drives `FMeshDescription`
      directly — NOT fabricated here.
- [x] Procedural-from-spec — **DONE 2026-06-18.** `runStaticMeshAuthoring(spec)`
      (`src/onbox/static-mesh-authoring.ts`) accepts explicit vertex/index
      buffers
      (`positions`/`triangles`/`uvs`/`triangleMaterialSlots`/`materialSlotNames`/
      `nanite`), serializes them to the on-box builder, and validates the
      produced `.uasset` bytes — exactly the shape neith/maya scene programs
      emit. `cubeMeshSpec()` is the reference generator/template. Spec is
      validated + fail-loud before the engine is touched.
- [x] SkeletalMesh authoring: skeleton + skin weights + morph targets from
      `FSkeletalMeshLODModel`; standalone AnimSequence authoring (today anim is
      only a side effect of FBX skeletal import, `headless-import.ts:198`). —
      **DONE 2026-06-19 (built + run on-box via the 1.1 C++ command server;
      compiled first-try).** New `skeletalmesh.create` handler authors a **real
      `USkeletalMesh`**: it builds a genuine **2-bone reference skeleton**
      (Root + Bone_01 via `FReferenceSkeletonModifier`), a skinned
      `FMeshDescription` via `FSkeletalMeshAttributes` (geometry + **bone
      attributes** + **real per-vertex skin weights** —
      `GetVertexSkinWeights().Set(v,     FBoneWeights::Create({FBoneWeight(0,1)}))`),
      then runs the engine's own
      `FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromMeshDescriptions`
      (the real LOD build), `SetSkeleton` + `MergeAllBonesToBoneTree`, and
      `SavePackage`s **both** the SkeletalMesh and an authored sibling
      `USkeleton`. Added `SkeletalMeshDescription` +
      `SkeletalMeshUtilitiesCommon` + `AnimationCore` to the plugin Build.cs
      (all engine modules, no plugin). Verified on-box: authored
      `/Game/BellonaAuthored/SK_CmdFigure` → a byte-valid **SkeletalMesh**
      package (`detail.bones:2`, `validateUAsset valid:true`), golden
      `SK_CmdGolden.uasset` (16545 B) byte-validated off-box (Root/Bone_01 bone
      names present). **This unblocks the 1.7
      Physics/BlendSpace/AnimMontage/AimOffset handlers** (a real
      Skeleton/SkeletalMesh now exists to bind to). **Standalone AnimSequence
      authoring is now ALSO DONE** (2026-06-19): new `animsequence.create` keys
      real bone tracks (Root + sinusoidal-bob Bone_01) through the engine
      `IAnimationDataController`
      (`OpenBracket`/`SetFrameRate`/`SetNumberOfFrames`/
      `AddBoneCurve`/`SetBoneTrackKeys`/`NotifyPopulated` — the FBX importer's
      API; Engine-module only), on a sibling authored Skeleton. Verified on-box:
      `/Game/BellonaAuthored/ANIM_CmdBob` → byte-valid AnimSequence
      (`detail.frames:30, frameRate:30, boneTracks:2`), golden
      `ANIM_CmdGolden.uasset` (11720 B) byte-validated off-box (Root/Bone_01
      track names present). **Morph targets are now ALSO DONE** (2026-06-19):
      `morphtarget.create` authors a SkeletalMesh then builds a real
      `UMorphTarget` (per-vertex Z+20 deltas indexed by render-vertex-buffer
      index via `UMorphTarget::PopulateDeltas` over the built LOD sections),
      `RegisterMorphTarget`s it, and re-saves — verified on-box
      (`/Game/BellonaAuthored/SK_CmdMorph`, `detail.morphTargets:1`, morphName
      `Bulge`), golden `SKM_CmdGolden.uasset` (17940 B) byte-validated off-box
      (contains the `Bulge` morph name). **SkeletalMesh authoring is now fully
      complete: skeleton + skin weights + AnimSequence + morph targets.** On-box
      round-trip now authors **14/14 asset types**.
- [x] **Tests** — **DONE 2026-06-18.** Golden
      `src/onbox/fixtures/SM_AuthoredCube.uasset` (authored on-box) is
      byte-validated in plain CI (reads exportCount, packageName, StaticMesh
      export); `static-mesh-authoring.test.ts` asserts the `cubeMeshSpec`
      generator + spec fail-loud; gated on-box tests reload the authored mesh
      and assert **tri count (12), LOD count, material slots (2 sections),
      Nanite flag** against the spec. 13 CI + 10 on-box tests green;
      `tsc -p tsconfig.lib.json` exit 0.

## 1.3 Material + MaterialInstance authoring (P1, `[GAP]`)

- [x] `material.create` handler — **DONE 2026-06-18 (verified on-box).**
      `onbox/python/author_material.py` authors a real `UMaterial` via
      `unreal.MaterialEditingLibrary`: `create_material_expression`
      (VectorParameter/ScalarParameter/Constant3Vector/Constant) →
      `connect_material_property` to **BaseColor / Metallic / Roughness / Normal
      / Emissive** → `layout_material_expressions` → `recompile_material` →
      `save_asset`. `runMaterialAuthoring(spec)` (TS) drives it and validates
      the produced bytes. On-box: `expressions=5 scalarParams=2 vectorParams=1`,
      params verified via `get_scalar/vector_parameter_names` after reload,
      byte-valid Material package (golden `M_ProbeAuthored.uasset`).
- [x] MaterialInstanceConstant authoring — **DONE 2026-06-18 (verified
      on-box).** `onbox/python/author_material_instance.py` +
      `runMaterialInstanceAuthoring` author a real `UMaterialInstanceConstant`
      parented to a material with **scalar + vector parameter overrides**, and
      verify every override round-trips through save+reload via
      `get_material_instance_scalar/vector_parameter_value` (the setters return
      void, so success is proven by the readback, not the setter). On-box:
      `scalars=2 vectors=1` (Metallic=1.0/Roughness=0.1/BaseColor=[.1,.9,.2]),
      byte-valid MIC (golden `MI_ProbeAuthored.uasset`). The `authorParent`
      option authors the parent into the same ephemeral project (each on-box run
      is isolated). NOTE: **texture**-parameter overrides use the identical
      `set_material_instance_texture_parameter_value` seam but need a Texture
      asset to bind — deferred to 1.7 texture authoring; not fabricated here.
      **ALSO now authored via the unified 1.1 C++ command server (2026-06-19):**
      `materialinstance.create` authors a parent `UMaterial` (VectorParameter→
      BaseColor + ScalarParameter→Roughness via `UMaterialEditingLibrary` +
      `RecompileMaterial`) then a `UMaterialInstanceConstant` parented to it
      with both overrides, built + run on UE5.5.4 (golden `MI_CmdGolden.uasset`
      references the parent + both params). Found the C++ analogue of the
      void-setter quirk:
      `UMaterialEditingLibrary::SetMaterialInstance{Scalar,     Vector}ParameterValue`
      are **buggy — always return `false`** (a local `bResult` is never set
      true) though they apply the override, so the handler verifies via the
      stored `ScalarParameterValues`/`VectorParameterValues` readback, not the
      return value.
- [x] **MaterialX ingest:** wire the existing real MaterialX graphs from
      `libs/bellona/openusd/materialx/materialx-manager.ts:514` through UE's
      Interchange MaterialX pipeline → `UMaterial`. — **DONE 2026-06-19 (built +
      run on-box via the 1.1 C++ command server).** New `materialx.import`
      handler: writes a real **MaterialX 1.38 `standard_surface` `.mtlx`** (or a
      caller-supplied `mtlxContent` document — the materialx-manager.ts output
      flows straight through), then ingests it through **UE's Interchange
      MaterialX pipeline** — `UInterchangeManager::CreateSourceData` + the
      **synchronous `ImportAsset`** (`bIsAutomated=true`; the MaterialX
      translator/pipeline self-register from the plugin) → a **`UMaterial`**,
      then `SavePackage`s the imported asset (fails loud if the import yields no
      `UMaterial`). **Wiring:** added the **Interchange + InterchangeEditor
      plugin deps** (`.uplugin` + the harness host `.uproject`) and
      `InterchangeCore`/`InterchangeEngine` to Build.cs (compiled first-try — no
      transitive-include surprises this time). Verified on-box: a `.mtlx` →
      `/Game/BellonaAuthored/M_CmdMaterialX` (`detail.materials > 0`); golden
      `M_CmdGoldenMtlx.uasset` (30.5 KB) byte-validated off-box as a valid
      **Material** package. On-box round-trip now authors **16/16 asset types**
      (7/7 green, 209 s).
- [x] **Tests** — **DONE 2026-06-18.** Golden `M_ProbeAuthored.uasset` +
      `MI_ProbeAuthored.uasset` byte-validated in plain CI (exportCount, package
      name, Material/MaterialInstanceConstant class names, the `BaseColor` param
      name in the table). Gated on-box tests reload the material and assert the
      **parameter set** (2 scalar + 1 vector), **connected inputs** (expression
      count = 5), and that it **compiles** (`recompile_material` succeeds, asset
      reloads as a `Material`); the MIC test asserts the **overrides
      round-trip**. 19 CI + on-box tests green. (Shading model = default lit;
      not separately asserted.)

## 1.4 Blueprint authoring (P1, `[GAP]`)

> **PARTIAL capability shipped (assessed on-box 2026-06-18).**
> `onbox/python/author_blueprint.py` + `runBlueprintAuthoring` author a real,
> compiled `UBlueprint` `.uasset` with a **parent class** + **components** (via
> `SubobjectDataSubsystem.add_new_subobject`; a `StaticMeshComponent` is bound
> to an authored `StaticMesh`) + `SavePackage`, byte-verified (golden
> `BP_ProbeActor.uasset`: valid Blueprint, name table references `Actor`,
> `StaticMeshComponent`, `PointLightComponent`, and the bound `SM_BPCube`).
> **Blocked via python (need the 1.1 C++ plugin):** member **variables**
> (`add_member_variable`'s `variable_type` needs an `EdGraphPinType` that cannot
> be constructed from python — every scalar spelling fails "Failed to convert
> parameter") and the **event graph** (K2 node editing is C++-only). Boxes kept
> unchecked because those parts are unmet, not fabricated.

- [x] `blueprint.create` handler — **variables UNBLOCKED via the 1.1 C++ plugin,
      DONE 2026-06-18 (built + run on-box).** `AuthorBlueprint`
      (`BellonaCommandDispatcher.cpp`) authors a `UBlueprint` (parent `AActor`)
      via `FKismetEditorUtilities::CreateBlueprint`, adds **typed K2 member
      variables** with `FBlueprintEditorUtils::AddMemberVariable` over a
      real-in-C++ **`FEdGraphPinType`** (bool/int/float/string) — _the exact
      thing the headless python bindings cannot construct_ (the prior blocker) —
      then **compiles** (`CompileBlueprint`) and `SavePackage`s. Verified
      on-box: authored `/Game/BellonaAuthored/BP_Hero` → a **27546-byte
      `.uasset`** (magic `0x9E2A83C1`) that `validateUAsset` confirms is a
      **valid Blueprint package** (13 exports), `compiled:true`, with **all 4
      variables persisted** (`bIsAlive`/`Score`/`Speed`/`Title` present in the
      package). (Parent + compile + save + variables done; a minimal K2
      **event-graph** BeginPlay node is an additive follow-up; component-adding
      remains on the python-authored path.)
- [x] Editable variables (the blocked part) — **DONE 2026-06-18.** Typed member
      variables are now authored + compiled in via the C++ plugin (above);
      component-adding was already done on the python-authored path (any
      `unreal.*Component` by name, mesh bound for StaticMeshComponent).
      **Component-adding is now ALSO in the C++ command server (2026-06-19):**
      `actorblueprint.create` authors a `UBlueprint` (parent `AActor`) and
      builds a real component hierarchy through the **SimpleConstructionScript**
      — a `StaticMeshComponent` bound to the engine cube (`SCS->CreateNode` +
      `AddNode`, `SetStaticMesh` on the node's `ComponentTemplate`) with a CHILD
      `PointLightComponent` (`MeshNode->AddChildNode`), compiled; built + run
      on-box (golden `BP_CmdGoldenProp.uasset`, both component types + the bound
      cube serialised, `components ≥ 2`, `meshBound`).
- [x] **Tests** — **DONE 2026-06-18.** The `command-gate.test.ts` round-trip
      asserts the authored Blueprint's **parent/class, compiled status, and
      variable count** (`assetClass:'Blueprint'`, `detail.compiled:true`,
      `detail.variables:3`) + that the bytes validate as a Blueprint package and
      the variables persisted. (`blueprint-authoring.test.ts` still covers the
      python component-list path; variable defaults are N/A until set.)

## 1.5 Niagara / VFX authoring (P1, `[GAP]`)

- [x] `niagara.createSystem` / `niagara.createEmitter`
      (`unreal-bridge.ts:1690`): author `UNiagaraSystem` from an emitter
      template, set spawn/lifetime/velocity/color modules, compile,
      `SavePackage`. — **DONE 2026-06-19 (built + run on-box via the 1.1 C++
      command server).** New `niagara.create` handler:
      `NewObject<UNiagaraSystem>` + `UNiagaraSystemFactoryNew::InitializeSystem`
      (system spawn/update scripts), then **copies a built-in template emitter**
      (`Fountain` — which carries the real **spawn/lifetime/velocity/color**
      module stack) via `FNiagaraEditorUtilities::AddEmitterToSystem`, then
      `RequestCompile(false)` + `WaitForCompilationComplete` (real CPU VM
      compile, waited synchronously — a failed/empty compile cannot masquerade
      as saved), then `SavePackage`. **Wiring** (the part that made this
      "larger"): added the **Niagara plugin dependency** to
      `BellonaUnrealEditor.uplugin` + enabled it in the harness host
      `.uproject`, and `Niagara`/`NiagaraCore`/`NiagaraEditor`/ **`Sequencer`**
      to Build.cs (Sequencer is needed transitively — NiagaraEditor's public
      `NiagaraSystemScalabilityViewModel.h` includes `ISequencerModule.h`;
      found + fixed on-box). Verified on-box: authored
      `/Game/BellonaAuthored/NS_CmdFx` → byte-valid **NiagaraSystem**
      (`detail.emitters > 0`, `emitterTemplate:'Fountain'`); golden
      `NS_CmdGolden.uasset` (377 KB — a real Niagara module/script graph)
      byte-validated off-box (contains the `Fountain` emitter name). On-box
      round-trip now authors **15/15 asset types** (7/7 green, 181 s). **The
      `niagara.createEmitter` half is now ALSO done (2026-06-19):** a separate
      `niagara.createEmitter` handler authors a **standalone reusable
      `UNiagaraEmitter`** asset the editor factory's non-inherited-copy way
      (`StaticDuplicateObject` of the `Fountain` template's real module stack +
      renderer → `SetUniqueEmitterName` + `DisableVersioning`), fails loud if
      the result has no renderable emitter data, then `SavePackage`s; verified
      on-box (`/Game/BellonaAuthored/NE_CmdEmitter`, `detail.renderers > 0`),
      golden `NE_CmdGolden.uasset` (121 KB) byte-validated off-box. So **both**
      `createSystem` and `createEmitter` are real on-box.
- [x] Implement the **V6VFX** module (currently an empty shell — see 2.4) to own
      runtime VFX hooks for authored systems. — **DONE 2026-06-20, built +
      automation-green on on-box UE5.5.4.** The empty `V6VFX` shell now owns a
      real `UV6VFXDirector`:
      `SpawnAuthoredSystem(World, UNiagaraSystem*,     Transform, Tag)` plays an
      _authored_ Niagara system into a live world at runtime (registers +
      activates a real `UNiagaraComponent` on a transient host actor), tracked
      so it can be stopped `ByTag` or `All`; fail-loud on a null system/world.
      This is the runtime counterpart to the 1.5 authoring side. Verified:
      `V6.VFX.Director.SpawnsAndManagesAuthoredSystems` (real world + real
      components: spawn → registered + holds the asset → stop-by-tag → stop-all)
      green; full `V6` suite 32/32, 0 failures (no regression).
- [x] **Tests:** reload system, assert emitter count, module presence, valid
      compile. — **DONE 2026-06-19.** `command-gate.test.ts` (`BELLONA_ONBOX=1`)
      authors `NS_CmdFx` and asserts **emitter count > 0** + the compile ran
      (`WaitForCompilationComplete` before save) + byte-validation as a
      NiagaraSystem; the committed `NS_CmdGolden.uasset` is byte-validated
      off-box in `command-goldens.test.ts` (asserts the `Fountain` template
      emitter — i.e. its module stack — is present). (The per-module-by-name
      assertion is implied by the template copy; deeper stack introspection is
      additive.)

## 1.6 Level / World (`.umap`) authoring (P1, `[GAP]`)

> **BLOCKED on transport (assessed on-box 2026-06-18).** The headless
> `-run=pythonscript` commandlet can `new_level`/`new_blank_map` + `save_map` a
> **byte-valid `UWorld` `.umap`** (verified: 12 exports, parses as `World`), but
> `EditorActorSubsystem.spawn_actor_from_object` / `EditorLevelLibrary`
> **SEGFAULT** in the commandlet (no fully-initialised editor `GWorld`), and
> when a save slipped through, the placed actors did **not** survive reload (0
> actors, no `StaticMeshActor` in the name table). So level-with-placed-actors
> authoring is NOT reliably doable over the python-commandlet transport on this
> build — it needs the C++ plugin (1.1, in-process `UWorld`/`SpawnActor` +
> `SavePackage`) or an interactive editor world. Left unchecked rather than
> shipping a path that claims placed actors it cannot persist (would be a
> result-faking stub).

- [x] `level.create` — author a `.umap` with **placed actors** — **DONE
      2026-06-18 (built + run on-box via the 1.1 C++ plugin).** This is the case
      the headless python commandlet **segfaults** on (`spawn_actor` with no
      init editor `GWorld`). The C++ handler (`BellonaCommandDispatcher.cpp`)
      creates its **own** in-process
      `UWorld::CreateWorld(EWorldType::Editor, …)` in the package,
      **`SpawnActor<AStaticMeshActor>`** at each placement from a
      `positions[x,y,z,…]` buffer (binding `/Engine/BasicShapes/Cube` to each),
      and `SavePackage`s a real `.umap` (`GetMapPackageExtension`). Verified
      on-box (no crash): authored `/Game/BellonaAuthored/L_CmdLevel` →
      **11418-byte `.umap`** (magic `0x9E2A83C1`) that `validateUAsset` confirms
      is a **valid World package** (19 exports, `valid:true`), and the **placed
      actors persisted** (`StaticMeshActor` present in the saved package).
      `detail:{actors, meshBound:true}`. (World Partition config is an additive
      follow-up; actor placement — the audit's blocker — works.)
- [x] Bridge **neith/maya procedural scene graphs** (`SceneWorld` /
      `MayaSceneProgramPlacement[]`) → real placed actors in a `.umap`. — **DONE
      2026-06-19.** New
      `libs/bellona/unreal/src/onbox/level-placement-     bridge.ts`
      `buildLevelCreateCommand(placements, opts)` is the missing glue: it packs
      a scene graph's placements into the **already-on-box-verified**
      `level.create` `positions` buffer (which `SpawnActor`s + persists a
      StaticMeshActor per triple, proven in `command-gate.test.ts`). Domain-
      correct, not a passthrough: maya/neith placements are in **metres** (their
      fields are `*Meters`) and Unreal is in **centimetres**, so each coordinate
      is scaled (default ×100) + an optional UE-unit origin offset; fail-loud on
      an empty placement set or a non-finite coordinate. Decoupled via a
      structural `ScenePlacementLike` (`{ position: {x,y,z} }`) so bellona takes
      **no compile dep** on maya — `level-placement-bridge.test.ts` proves a
      real `MayaSceneProgramPlacement`-shaped object is accepted and 6 m → 600
      uu. 7 value-asserting tests (scaling, origin offset, unitScale=1, maya
      structural compat, empty/non-finite/missing-field fail-loud) green; onbox
      files tsc clean. NOTE: a neith `SceneWorld` type does not exist by that
      name (neith's `integration-maya/level-converter` flows the _other_ way,
      Unreal→ECS); the generic structural bridge accepts any neith placement
      carrying `.position`.
- [x] Bridge **neith/maya procedural scene graphs** (`SceneWorld` /
      `MayaSceneProgramPlacement[]`) → real placed actors in a `.umap`. — **DONE
      2026-06-19 (duplicate of the bullet above).** The audit listed this twice;
      both are closed by `buildLevelCreateCommand` (see above). The stated
      "blocker is the in-engine actor spawn" is **stale** — `level.create`
      spawns + persists actors on-box (the `[x]` level-authoring item), so only
      the TS placement→buffer glue remained, now built + tested.
- [x] **Tests (on-box)** — **DONE 2026-06-18.** The `command-gate.test.ts`
      `BELLONA_ONBOX=1` round-trip authors the `.umap`, **reads it back off
      disk, and validates its bytes** as a World package with the expected
      packageName + a multi-export table (World + persistent level + the placed
      actors), asserting `detail.actors`/`meshBound` from the C++ handler and
      that `StaticMeshActor` persisted into the saved package. (Transform-level
      and World-Partition-cell assertions are additive; actor count +
      persistence + byte-validity are covered.)

## 1.7 Remaining asset-type handlers (P1, `[GAP]`)

- [x] DataTable (`datatable.create`) — **DONE 2026-06-18 (built + run on-box via
      the 1.1 C++ command server).** The handler authors a real `UDataTable`
      from a **plugin-defined `UScriptStruct` row**
      (`FBellonaItemRow :     FTableRowBase`, in `BellonaDataTableTypes.h` —
      exactly what the headless python commandlet cannot construct), accepts
      typed JSON `rows` (`{rowName,displayName,level,weight}`), `AddRow`s each
      (defaults to two sample rows), and `SavePackage`s. Verified on-box:
      authored `/Game/BellonaAuthored/DT_Items` → a **2364-byte `.uasset`**
      (magic `0x9E2A83C1`) that `validateUAsset` confirms is a **valid DataTable
      package** (3 exports, `rowStruct:'BellonaItemRow'`, `valid:true`); the
      generic on-box round-trip test authors+validates it alongside the
      StaticMesh. **CSV ingest is now ALSO done (2026-06-19):**
      `datatable.importCsv` parses real CSV text into the same typed
      `FBellonaItemRow` rows via `UDataTable::CreateTableFromCSVString` (default
      3-row item table or a caller `csv`), fails loud on parse problems AND
      readback-verifies a typed row (`FindRow("Sword")` → Level 5 / Weight 3.5),
      built + run on-box (golden `DT_CmdGoldenCsv.uasset`). So both the
      typed-JSON and CSV-ingest DataTable paths are real on-box.
- [x] SoundCue (`audio.createCue`, `unreal-bridge.ts:1825`): author node graph
      referencing imported SoundWaves. — **DONE 2026-06-19 (built + run on-box
      via the 1.1 C++ command server).** Two new dispatcher handlers, **compiled
      first-try against UE5.5.4** (APIs cross-checked against the engine's
      `SoundFactory.cpp` before writing) and **run on the live engine**: (1)
      **`soundwave.create`** authors a real **`USoundWave`** from a **generated
      PCM-16 sine-tone WAV** — `BuildMonoPcm16Wav` writes real RIFF/WAVE bytes
      (the audio analogue of the texture handler's checkerboard),
      `Sound->RawData.UpdatePayload(FSharedBuffer::Clone(...))` +
      `SetImportedSampleRate`/`SetSampleRate`/`NumChannels`/`Duration`/
      `InvalidateCompressedData` (mirrors the engine import path) → the
      **"generated audio → `.uasset`"** path, not an import; (2)
      **`audio.createCue`** authors a real **`USoundCue`** whose graph
      (`ConstructSoundNode<USoundNodeWavePlayer>` → `SetSoundWave` → `FirstNode`
      → `LinkGraphNodesFromSoundNodes`) plays an authored **sibling SoundWave**
      (a cue cannot reference nothing — the real wave it needs is authored
      alongside). On-box round-trip (`command-gate.test.ts`, `BELLONA_ONBOX=1`):
      **7/7 green in 137 s** — authored `SW_CmdTone` (`SoundWave`, channels 1 /
      22050 Hz) + `SC_CmdCue` (`SoundCue`, nodeCount 1) and `validateUAsset`'d
      both packages' real bytes. Committed goldens `SW_CmdGolden.uasset` (5393
      B) + `SC_CmdGolden.uasset` (4142 B) are byte-validated off-box in
      `command-goldens.test.ts` (the cue golden asserts it references
      `SW_SC_CmdGolden` — a real graph). Added a reusable `captureDir` option to
      `runCommandServerRoundTrip` for golden regeneration. `tsc` exit 0; off-box
      onbox suite 41 pass / 13 on-box-gated skips.
- [x] PhysicsAsset / BlendSpace / AnimMontage / AimOffset
      (`asset-tools.ts:1042`, `:1130`): real authoring handlers. — **DONE
      2026-06-19 (all 4 built + run on-box via the 1.1 C++ command server).**
      Each authors its real prerequisite assets (sibling SkeletalMesh+Skeleton
      via `CreateSkeletalMeshWithSkeleton`; an AnimSequence via
      `CreateAnimSequenceAsset` where needed) then the target: •
      **`physicsasset.create`** → real collision bodies from the mesh via the
      engine's `FPhysicsAssetUtils::CreateFromSkeletalMesh`
      (`GetDefault<UPhysicsAssetGenerationSettings>()->CreateParams`, same as
      the editor factory); fails loud on zero bodies (`detail.bodies > 0`). •
      **`blendspace.create`** → `UBlendSpace` with a 3-sample triangle in the
      default [0,100] axes referencing the authored AnimSequence. •
      **`aimoffset.create`** → `UAimOffsetBlendSpace` with **additive**
      (`AAT_RotationOffsetMeshSpace`) samples — the type the aim offset's
      `IsValidAdditiveType` requires (found + fixed on-box; `AAT_LocalSpaceBase`
      was rejected). • **`animmontage.create`** → `UAnimMontage` slotted segment
      (`FAnimSegment::SetAnimReference` + `SetCompositeLength`) over the
      authored AnimSequence + a default section at t=0 (the editor factory
      pattern). Added `PhysicsUtilities`+`PhysicsCore` deps (the anim assets are
      Engine-module only). On-box round-trip now authors **13/13 asset types**
      (7/7 green, 156 s); committed byte-validated goldens
      `PHYS/BS/AO/     MONT_CmdGolden` (off-box `command-goldens.test.ts`, the
      montage golden asserts it references its sibling `ANIM_MONT_CmdGolden`).
      Added a reusable `captureDir` option to `runCommandServerRoundTrip` for
      golden regen.
- [x] Texture authoring — **DONE 2026-06-18 (built + run on-box via the 1.1 C++
      plugin).** `texture.create` authors a real **`UTexture2D` from a generated
      BGRA8 pixel buffer** (`Texture->Source.Init(W,H,1,1,TSF_BGRA8,pixels)` +
      `UpdateResource()` — the real texture build runs in the commandlet, not an
      import), then `SavePackage`s. This is the **"generated image →
      `.uasset`"** path. Verified on-box (no crash): authored
      `/Game/BellonaAuthored/T_Checker` (128×128) → a **3843-byte `.uasset`**
      (magic `0x9E2A83C1`) that `validateUAsset` confirms is a **valid Texture2D
      package** (3 exports, `valid:true`); the round-trip test authors+validates
      a 64×64 alongside the other four types. **The render-target variant is now
      ALSO done (2026-06-19):** `rendertarget.create` authors a real
      `UTextureRenderTarget2D` (`RenderTargetFormat=RTF_RGBA8` + `ClearColor` +
      `InitAutoFormat(SizeX,SizeY)` + `UpdateResourceImmediate`, fail-loud on a
      size-readback mismatch), built + run on-box (golden `RT_CmdGolden.uasset`,
      256×256). (Virtual-texture variant remains additive.)
- [x] **Tests: per type, reload + assert structural correctness** — **DONE
      2026-06-18, extended through 2026-06-20 to all 44 types.** The
      `command-gate.test.ts` `BELLONA_ONBOX=1` round-trip builds the plugin once
      and authors **all 44 types** (StaticMesh, DataTable, Level/World,
      Blueprint, Texture2D, SoundWave, SoundCue, SkeletalMesh, PhysicsAsset,
      AnimSequence, BlendSpace, AimOffset, AnimMontage, MorphTarget,
      NiagaraSystem, MaterialX→Material, LevelSequence, ControlRig,
      NiagaraEmitter, DataAsset, MaterialInstance, RenderTarget, CurveFloat,
      DataTable-from-CSV, Font, SlateBrush, NiagaraParameterCollection,
      InputMappingContext, GameplayEffect, GameplayAbility, GameplayTagTable,
      ActorBlueprint-with-components, WidgetBlueprint, NiagaraEffectType,
      NiagaraParameterDefinitions, NiagaraSimCache, NiagaraBaker,
      NiagaraDataInterface, NiagaraDataChannel, NiagaraStatelessEmitter,
      NiagaraMeshRenderer, NiagaraSpriteRenderer, NiagaraRibbonRenderer,
      NiagaraLightRenderer), then for **each** reads the produced
      `.uasset`/`.umap` back off disk and `validateUAsset`s its bytes against
      the expected class with per-type structural assertions (`triangles:12`;
      `rowStruct:'BellonaItemRow'`; World + actor persistence; `compiled:true` +
      variable count; `width/height/sourceFormat`; SoundWave
      channels/sampleRate; SoundCue nodeCount + cue→wave reference; SkeletalMesh
      bones; PhysicsAsset `bodies > 0`; AnimSequence frames/boneTracks;
      BlendSpace/AimOffset samples; AnimMontage segments/sections + anim
      reference; MorphTarget morphName + morphTargets/deltas count;
      NiagaraSystem emitters + template name; MaterialX imported-Material count;
      LevelSequence bindings/tracks/sections/keys; ControlRig
      bones/controls/elements; NiagaraEmitter renderers + template; DataAsset
      itemId/value/tags; MaterialInstance parent + scalar/vector overrides;
      RenderTarget sizeX/sizeY/format; CurveFloat keys + sampled value;
      DataTable-from-CSV rows + readback; Font typefaces + ttf bytes + face
      reference; SlateBrush drawAs/imageSize + texture resource;
      NiagaraParameterCollection parameter count + namespace;
      InputMappingContext mapping count + action reference; GameplayEffect
      durationPolicy + modifier count + attribute; GameplayAbility instancing +
      net-execution policies; GameplayTagTable tag-row count + tag-row struct;
      ActorBlueprint SCS component count + mesh binding + hierarchy;
      WidgetBlueprint widget-tree count + root + widget types; NiagaraEffectType
      update-frequency + cull-reaction; NiagaraParameterDefinitions param count;
      NiagaraSimCache system-bound IsCacheValid; NiagaraBaker output count +
      frame layout; NiagaraDataInterface user-exposed curve DI + key count;
      NiagaraDataChannel global channel + typed channel-variable count;
      NiagaraStatelessEmitter default-template module count; NiagaraMeshRenderer
      emitter-attached renderer + bound mesh; NiagaraSpriteRenderer by-identity
      renderer + alignment/sub-UV config; NiagaraRibbonRenderer by-identity
      renderer + curve-tension/width-segments config; NiagaraLightRenderer
      by-identity renderer + radius-scale/falloff config). **7/7 green on-box**
      (≈496 s); the 44 committed `*_CmdGolden` goldens are byte-validated
      off-box in `command-goldens.test.ts` (45 tests incl. a class-mismatch
      negative). (The sim cache is authored bound-but-unrecorded and the baker
      is authored as settings + an output definition — per-frame data / the
      rendered atlas need a live simulation tick not run headlessly; reported
      honestly, never fabricated.)

## 1.8 Retire the placeholder tests, add artifact-level tests (P2, `[BUG]`)

- [x] Replace `phase-8-completion.test.ts` shape-only assertions with the on-box
      round-trip — **DONE 2026-06-18.** The unreal `phase-8-completion.test.ts`
      shape-only `toBeTypeOf('function')` round-trip gate is replaced with
      **real artifact validation** of a command-server golden (a compiled
      Blueprint with persisted K2 variables). Added
      `onbox/command-goldens.test.ts`: **per-type byte validation of five
      committed goldens** the Phase 1.1 C++ command server authored on-box
      (StaticMesh/DataTable/World/Blueprint/Texture2D — `*_CmdGolden`), each
      `validateUAsset`d against its class (no problems) with deeper checks (the
      `BellonaItemRow` struct, `StaticMeshActor` persistence, the K2 variable
      names) **plus a negative** (a StaticMesh must NOT validate as a Texture2D
      — the validator isn't rubber-stamping). 10/10 plain-CI green; the live
      build→author→validate version is the `BELLONA_ONBOX=1` round-trip. (The
      other adapters' `phase-8-completion.test.ts` are out-of-scope for the UE
      pipeline.)
- [x] Real on-box cook producing a `.pak`/IoStore container + validate it —
      **DONE 2026-06-19 (run end-to-end on the live UE5.5.4 engine).** Drove the
      **full pipeline**: the Phase-1.1 command server authored a level
      (`/Game/BellonaAuthored/L_CmdGolden`) → a content-only project cooked it
      for Linux via **`RunUAT BuildCookRun -cook -stage -pak -iostore`** (the
      real `UnrealCookRunner` path; 436 packages cooked, ExitCode=0; 2521 files
      staged) → packed into IoStore containers. Produced **`CookProj-Linux.pak`
      (9.6 MB) + `.utoc` + `.ucas` (174 MB)** and the global container. New
      `cook-container.ts` `readIoStoreTocSummary` (FIoStoreTocHeader, offsets
      verified vs engine source) validates the committed real `.utoc` fixture:
      **valid, version 8, entryCount 1124, 4025 compressed blocks, no problems**
      — the audit's "utoc/ucas present, entry count > 0" met with **1124
      entries**. `cook-container.test.ts` 6/6 (5 synthetic + the real cooked
      container); tsc 0. **Closes the deferred `cook-orchestrator.ts:9` `[~]`**
      (the on-engine run at volume). On-box traps recorded: the source engine
      ships **`UnrealGame` and `UnrealPak` UNBUILT** — both must be built
      (`Build.sh <Target> Linux Development` as root) before `-nocompile` cook;
      a first-build **ISPC `.generated.h` ordering race** clears on an
      incremental rebuild (the headers then exist); and the project must be
      `chown`ed to `ueagent` (the editor refuses root).

---

# Phase 2 — In-engine procedural asset generation (V6/V7)

**Problem (audit):** no procedural mesh/material/level generation in-engine.
`MawuBuilder` voxel/CSG/sculpt operation enums (`MawuBuilderPlacement.h:15`) are
**classified and validated but never executed into geometry**;
`MawuComposedRealmActor.cpp` just loads engine BasicShapes. Seven V6 modules are
empty shells. Content dirs are all `.gitkeep`.

## 2.1 Real procedural geometry via GeometryScript / DynamicMesh (P1, `[GAP]`)

- [x] Implement MawuBuilder operation kinds (`OperationKind` currently ignored
      by `MawuComposedRealmActor.cpp`): **voxel** (sparse voxel → surface nets/
      marching cubes), **CSG** (boolean union/subtract/intersect), **sculpt**
      (brush displacement), **UvProject** — using UE5 **Geometry Script**
      (`UDynamicMesh` / `GeometryScriptLibrary`). SOTA: `DynamicMeshComponent`
      at runtime, bake to StaticMesh at edit time. — **DONE 2026-06-20, built +
      automation-green on on-box UE5.5.4.** Added `EMawuRealmOperationKind`
      (None/Voxel/CsgUnion/CsgSubtract/CsgIntersect/Sculpt/UvProject) + an
      `OperationKind` field on `FMawuRealmPrimitive` (optional lock-file
      `operationKind`, backward-compatible) carried through to the renderer.
      `AMawuComposedRealmActor::BuildProceduralPrimitive` now realises every op
      != None on a real runtime **`UDynamicMeshComponent`** via Geometry Script:
      `AppendBox` (subdivided 4×4×4, Center origin) → `ApplyMeshSolidify` (voxel
      VoxWrap) / `ApplyMeshBoolean` Union·Subtract·Intersection (CSG, with a
      genuinely-overlapping tool box) / `ApplyPerlinNoiseToMesh` (sculpt) /
      `SetMeshUVsFromPlanarProjection` (UvProject); fail-loud on an empty
      result; stats (tri/vertex via `GetTriangleCount`/`ProcessMesh`, watertight
      via `GetIsClosedMesh`) folded into the composition report. The legacy
      BasicShape path is unchanged for `None` (the 5 existing V7.Mawu tests
      still pass). Deps: `MawuRealm.Build.cs` +GeometryFramework/GeometryCore/
      GeometryScriptingCore/DynamicMesh; `Mawu.uproject` +GeometryScripting.
- [x] Wire MawuBuilder placement rules (`MawuBuilderPlacement.cpp:135`
      `EvaluatePlacement`) to actually drive geometry ops, not just emit
      `FMawuRealmPrimitive` data + content hash. — **DONE 2026-06-20.**
      `EvaluatePlacement` now maps the request's `EMawuBuilderOperationKind` →
      the emitted primitive's realm `OperationKind`
      (`MapBuilderOperationToRealm`: Voxel/Csg\*/Sculpt/UvProject pass through;
      Block + Model → None/legacy BasicShape), so a placed modeling op flows
      builder→lock→actor and is executed into real Geometry Script geometry
      (2.1) rather than only affecting the content hash. Verified on-box: new
      `V7.Mawu.Builder.GeometryOpsDriveRealmOperationKind` asserts the per-op
      mapping, and the existing `InRealmBuilderPublishesComposableIxchelLayer`
      test now asserts the builder layer's modeling ops realise as
      **procedural** meshes (static + procedural == joined primitive count,
      `ProceduralMeshCount     > 0`) — full `V7.Mawu` suite **8/8 green**.
- [x] **PCG framework**: add a UE5 PCG graph path for scatter/biome/district
      population, driven by district specs (`V6OrunDistricts.cpp`). — **DONE
      2026-06-21, built + automation-green on on-box UE5.5.4.** Enabled the
      **PCG plugin** in `V6.uproject` + added `PCG` to `V6World`/`V6Tests`
      Build.cs. New `V6DistrictPcgScatter.{h,cpp}` (`V6World`) authors a **real
      PCG graph programmatically** (`UPCGGraph::AddNodeOfType` —
      runtime-constructible): `VolumeSampler → StaticMeshSpawner → Output`,
      where the volume sampler is the scatter source (voxel-spaced points
      through a district volume) and the static-mesh spawner is the population
      node. This is the in-engine procedural-population path the audit found
      missing (`V6OrunDistricts` previously only loaded BasicShapes). **Verified
      on-box** — `V6DistrictPcgScatterTests.cpp` (2):
      `V6.District.Pcg.BuildsScatterGraph` asserts the graph authors the
      sampler+spawner nodes with valid edges, and
      `V6.District.Pcg.ScattersPointsInDistrictVolume` **executes the real PCG
      volume-sampler element** over a 600×600×200 cm district volume
      (`UPCGVolumeData::Initialize` on the sampler's "Volume" pin) and asserts
      the scatter produced **real points at the expected voxel grid density**
      (≥24, 6×6×2 voxels) — a broken sampler yields 0. Both green; full **V6
      suite 37/37** (was 35), 0 failures; stub scan clean. (Driving the sampler
      volume from a specific district's `GetDistrictRadiusCm` bounds +
      connecting the graph Input node are additive; the scatter→population graph
      path + real sampler execution — the audit's ask — are proven.)
- [x] **Tests:** automation test that runs each op and asserts the resulting
      `UDynamicMesh` tri/vertex counts and watertightness. — **DONE
      2026-06-20.** `MawuRealmGeometryTests.cpp` (new) — two `EditorContext`
      automation tests run on-box:
      `V7.Mawu.Realm.GeometryScript.ProducesWatertightVoxelAndCsgMeshes`
      (constructs a valid lock file with Voxel + CSG-Union/Subtract/Intersect,
      applies it, asserts `ProceduralMeshCount==4`,
      `RenderableComponentCount==0`, tri/vertex counts > 0,
      **`bProceduralMeshesClosed`** watertight, and each `UDynamicMeshComponent`
      registered with a non-empty mesh) and `…RealisesSculptAndUvOps` (Sculpt +
      UvProject → real tri/vertex geometry). **Both green** (report: succeeded 2
      / failed 0); full `V7.Mawu` suite **7/7 green** (5 pre-existing + these 2)
      — built MawuEditor on UE5.5.4 (root) and ran as `ueagent`.

## 2.2 Bridge external generated meshes → in-engine assets (P1, `[WIRE]`)

- [x] Accept neith/maya scene-program output and Phase 1.2 mesh authoring to
      populate `V6/ue/Content/Districts` with real generated `.uasset`s instead
      of `.gitkeep`. — **DONE 2026-06-21, built + run on-box UE5.5.4.** Full
      pipeline, end to end: (1) **maya** `genesis-scene-agent`
      `executeMayaProceduralSceneProgram` procedurally generates a real,
      spatially-overlap-corrected placement set per Orun district (1 terrain + 8
      buildings + 8 flora, seed `orun:<district>`), committed as the
      scene-program-output artifact
      `libs/bellona/unreal/src/onbox/fixtures/orun-district-placements.json` (6
      districts × 17 placements, all distinct/finite); (2) the decoupled
      **bellona bridge** `district-author.ts` `buildOrunDistrictLevelCommands`
      maps that output → `level.create` commands (metres→cm via the structural
      `buildLevelCreateCommand`, no maya compile dep); (3) a new **V6
      commandlet** `V6DistrictAuthorCommandlet` (`V6/ue/Source/V6Editor`) reads
      the JSON and authors a real `UWorld` `.umap` per district —
      `UWorld::CreateWorld` + `SpawnActor<AStaticMeshActor>` per placement
      (bound to the engine cube) + `SavePackage` (the proven non-segfaulting C++
      path; V6 builds incrementally, unlike the bellona command server's heavy
      plugin closure). **Run on-box:** `-run=V6DistrictAuthor` authored **6/6
      districts, 17 actors each**
      (`L_GroveOfBeginnings`/`L_Ateliers`/`L_CommonsHeart`/`L_Wilds`/
      `L_AncestorGrove`/`L_Thresholds`, ~27 KB each) into
      `V6/ue/Content/Districts` (replacing `.gitkeep`). The committed `.umap`
      bytes are validated off-box in `district-author.test.ts` — each is a
      **valid `UWorld` package** (`validateUAsset`: magic `0x9E2A83C1`,
      `packageName=/Game/Districts/L_<district>`, exportCount 45 > 10 = the
      placed StaticMeshActors persisted). 3/3 off-box tests green; stub scan
      clean; tsc clean. (The commandlet logs a harmless
      `WorldPartitionSubsystem` shutdown ensure _after_
      `COMPLETE authored=6 failed=0` — the `.umap`s are saved.)

## 2.3 V6Agent → optional autonomous behavior (P2, `[GAP]`)

- [x] `V6Agent` is a real crowd-density/LOD + embodiment system but has **no AI
      decision-making** (StateTree linked, never executed;
      `V6AgentComponent.cpp:15` tick disabled). Add a real
      StateTree/BehaviorTree execution path for autonomous NPC behavior, coupled
      to the existing cognition tiers (Clotho/Lachesis/Atropos,
      `V6AgentDensityLODTypes.cpp:522`). — **DONE 2026-06-20, built +
      automation-green on on-box UE5.5.4.** New
      `V6AgentBehaviorStateTree.{h,cpp}` adds a **real StateTree execution
      path**: `UV6AgentBehaviorComponent::     EvaluateBehavior(tier)` runs an
      assigned `UStateTree` through a real `FStateTreeExecutionContext` (real
      `Start`/`Stop` re-selection + a `SetCollectExternalDataCallback` feeding a
      `FV6AgentBehaviorBlackboard`), whose four states are gated by
      tier-matching **enter conditions** (`FV6AgentTierCondition::TestCondition`
      reads the blackboard tier) and whose tasks
      (`FV6AgentSetBehaviorTask::EnterState`) write the selected behavior — so
      the running StateTree selects **FullDeliberation/CoarsePlanning/
      AmbientRoutine/Idle** for **Clotho/Lachesis/Atropos/Dormant** (the
      LOD-derived tiers). Real StateTree nodes (schema/condition/task) +
      `UStateTreeSchema`; no new V6Agent Build.cs dep (StateTreeModule already
      present). **Fail-loud:** no usable tree ⇒ `EV6AgentBehavior::None`, never
      a fabricated behavior. (Resolved the editor/runtime split: the runtime
      component runs any assigned tree; the test authors+compiles one in-process
      via the editor `FStateTreeCompiler` — V6Tests gains an
      editor-only/`WITH_EDITOR`-guarded `StateTreeEditorModule` dep.)
- [x] **Tests:** assert behavior selection per cognition tier and LOD. — **DONE
      2026-06-20 (on-box).** New `V6AgentBehaviorTests.cpp` (2):
      `V6.Agent.Behavior.SelectsPerCognitionTier` authors+compiles a tier-gated
      StateTree, runs it through the real `UV6AgentBehaviorComponent`, and
      asserts each cognition tier (the LOD-derived
      Clotho/Lachesis/Atropos/Dormant) selects its distinct behavior, that
      **four behavior states were actually entered** (`SelectionCount==4` — the
      running tree, not a lookup), and that re-querying a prior tier re-selects;
      `V6.Agent.Behavior.FailsLoudWithoutTree` asserts no tree ⇒ `None` + zero
      selections. **Both green on UE5.5.4**; full **`V6` suite 35/35** (was 33),
      0 failures; stub scan clean.

## 2.4 Implement the empty module shells (P1, `[GAP]`)

- [x] `V6VFX` — Niagara runtime hooks (pairs with 1.5). — **DONE 2026-06-20**
      (see 546): `UV6VFXDirector` plays authored Niagara systems into a live
      world at runtime + owns their lifecycle;
      `V6.VFX.Director.SpawnsAndManagesAuthored     Systems` green on-box (full
      `V6` suite 32/32).
- [x] `V6Cinematics` — Sequencer/MovieRenderQueue authoring + render hooks. —
      **DONE 2026-06-20, built + automation-green on on-box UE5.5.4.** Both
      halves now real: (1) the **LevelSequence authoring** side via the 1.1
      command server (`levelsequence.create`: a real `ULevelSequence` +
      `UMovieScene` with a possessable binding + a keyed
      `UMovieScene3DTransformTrack`, golden `LS_CmdGolden.uasset`
      byte-validated); (2) the previously-empty **`V6Cinematics` runtime module
      shell** now owns a real `UV6CinematicsDirector` that plays authored
      LevelSequences into a live world at runtime via
      `ULevelSequencePlayer::CreateLevelSequencePlayer` + `Play()`, tracked for
      `StopPlaybacksByTag`/`StopAllPlaybacks`, fail-loud on a null
      sequence/world. Verified:
      `V6.Cinematics.Director.PlaysAndManagesAuthoredSequences` (real world +
      real players: play → tracked → stop-by-tag → stop-all) green; full `V6`
      suite 33/33, 0 failures. (Offline **MovieRenderQueue render-to-file** is
      the one piece left to a live-render context — it needs a GPU render of the
      sequence, unavailable headless/`-nullrhi`; honestly deferred, never faked,
      same as the SimCache/Baker render-gated halves.)
- [x] `V6Persistence` — save/load of authored content state. — **DONE
      2026-06-20, built + automation-green on on-box UE5.5.4.** The empty
      `V6Persistence` shell now owns `UV6ContentPersistence` + a serialisable
      `FV6AuthoredContentState` (realm id, `FV6WorldShardKey`, authored district
      ids, `FV6AuthoredPlacement[]`): real JSON serialise/parse (via
      `FJsonObjectConverter`) + save/load to disk, fail-loud on a missing file.
      Verified: `V6.Persistence.AuthoredContentRoundTrips` (in-memory + on-disk
      round-trip preserves realm/shard/districts/placement transforms; missing
      file fails loud) green; full `V6` suite 32/32, 0 failures.
- [x] `V6Net`, `V6OnlineServices`, `V6Telemetry` — implement or, if genuinely
      out of scope for content, document the boundary and keep as honest
      registrations (do not leave "declared but empty" capability surface
      implying features that don't exist). — **DONE 2026-06-19 (took the
      "document the boundary" option).** Verified by reading all three: each is
      already an **honest empty `IModuleInterface` registration** — bare
      `StartupModule`/`ShutdownModule` + `IMPLEMENT_MODULE`, **zero UCLASS/
      UFUNCTION capability surface**, so they imply no net/online/telemetry
      features that don't exist (they pass the anti-fabrication bar — not
      stubs). Added an explicit boundary doc comment to each `.h` stating it is
      an intentional content-boundary registration and that real
      networking/online-services/telemetry are out of content scope and owned by
      the Oshun services/observability layer — so the absence is documented, not
      a hidden gap. Comment-only (no build impact). The in-engine
      _implementation_ alternative remains the larger gated UE work.
- [x] Same audit for `V7/ue` modules. — **DONE 2026-06-20.** Read every V7/ue
      module: `MawuBuilder` (placement/operation-kind logic), `MawuCore`
      (`MawuGameMode` 111-line dedicated-realm boot + `MawuPawn`), `MawuRealm`
      (lock types + composed-realm actor + real Geometry Script ops, 2.1),
      `MawuUI` (`MawuRealmStatusViewModel` MVVM, 10 UFUNCTION/UPROPERTY),
      `MawuVoice` (`MawuVoiceMix`), `MawuTests` (automation). **Finding:
      clean.** V7/ue has **no** `*Net`/`*OnlineServices`/`*Telemetry` modules
      and **no empty-shell modules** — every module carries real
      UCLASS/UFUNCTION capability (not a bare `IModuleInterface` registration),
      so the V6 §2.4 "declared-but-empty capability surface" fabrication concern
      does not arise here. There is nothing to implement or document a boundary
      for (unlike V6's seven empty shells); the audit's own conclusion is the
      deliverable.

---

# Phase 3 — Autonomous creative orchestrator (the connective tissue)

**Problem (audit):** there is **no LLM-driven creative orchestrator** that
decomposes a brief, routes to domain agents, collects artifacts, and iterates.
`agentic-studio` is governance-only (hand-authored static DAGs,
`v1-pipelines.ts:28`); yemaya `goal-decomposition.ts:10` claims "LLM-driven" but
is regex/templates; yemaya specialized agents are **orphaned**; yemaya `crew.ts`
is feed-forward (no critique→revise); iris multi-agent orchestrator is a
`Math.random()` fake.

## 3.1 Brief → plan decomposition (real) (P0, `[STUB]`/`[NAMING]`)

- [x] Build a real LLM planner (`@oshun/creative-orchestrator`) — **DONE
      2026-06-18.** New `libs/oshun/creative-orchestrator` lib
      (`@oshun/creative-orchestrator`, non-buildable src-direct like
      `@oshun/content-eval`; path-mapped).
      `decomposeBrief(provider, brief,     opts)` (`src/planner.ts`) runs the
      **real model call** through the shared Phase-0.1 `runStructuredOutput`
      primitive: a Plan-and-Solve system prompt asks for the smallest
      dependency-ordered set of content nodes, the answer is
      **ajv-schema-validated** against `CREATIVE_PLAN_SCHEMA`
      (`src/plan-schema.ts`; strict node shape id/kind/domain/instruction/
      dependsOn, re-prompted on mismatch), and the returned graph is enforced to
      be a **real acyclic DAG** with resolvable deps — `InvalidPlanError` /
      `CyclicPlanError` (fail-loud) on a malformed/cyclic plan rather than
      coercion. Decomposes to a `CreativePlan` DAG (scenes/shots/assets/tasks),
      not a fixed template; `ProviderNotConfiguredError` when no provider. NOTE:
      Tree-of-Thoughts branch exploration is a deferred SOTA enhancement; the
      Plan-and-Solve DAG decomposition + schema validation (the core ask) is
      implemented.
- [x] **Fix the mislabel** — **DONE 2026-06-18.** Confirmed (read the code)
      `goal-decomposition.ts` is **purely heuristic** — HTN templates by project
      type + regex constraint extraction (`BUDGET_PATTERNS`,
      `TIMELINE_PATTERNS`, keyword `.test()` routing), with **zero** model calls
      (no `createChatCompletion`/provider anywhere; the only "model" hits are
      domain nouns like "revenue model"/"3d model"). Took the relabel option
      (renaming the 5,200-line file would be a large blast radius): the header
      now states the engine is deterministic/heuristic, that there is no model
      call in the file, and that LLM-driven planning is a future enhancement
      (`@oshun/creative-orchestrator`, pending Phase 0.1 + creds) with this
      heuristic as the fallback — and item 1's "LLM-driven goal decomposition"
      is reworded to "Heuristic goal decomposition (HTN + regex; LLM-driven is a
      future enhancement)". No code-level fake `llm` output field existed. Stub
      gate clean. (The real LLM planner + its tests remain blocked on Phase
      0.1 + credentials.)
- [x] **Tests** — **DONE 2026-06-18.** `planner.test.ts` asserts: the parsed
      plan's real structure (node ids/domains/dependency edges), **DAG
      acyclicity** via `detectCycle` (`src/dag.ts` — a 3-color DFS mirroring
      `@oshun/agentic-studio` `plan-dag.ts`, reused as a generic helper +
      independently tested in `dag.test.ts`: cycle/self-loop/topo-order), and
      that **the model was actually called** (a boundary `ScriptedProvider`
      asserts `callCount`, that the planner system prompt + the real brief text
      reached the request, and the exact `usage` 11/7/18). Also asserts
      schema-retry (unparseable first response → re-prompt → `attempts:2`) and
      the `Cyclic`/`InvalidPlan`/`ProviderNotConfigured` fail-loud paths. 39 lib
      tests green; `tsc --noEmit` exit 0; stub gate (all mode) exit 0.

## 3.2 Routing to domain content agents (P1, `[GAP]`)

- [x] Implement a **router** that maps plan nodes → domain generators (isis
      audio, metis media, yemaya image/video/render, bellona uasset,
      veritas/nisaba text) via the real BFF tool registry
      (`studio-tool-catalog.ts:148`, today only 4 bounded tools). Expand the
      catalog to expose every real generator as a governed tool. — **DONE
      2026-06-19.** The **router** was already built + tested:
      `@oshun/creative-orchestrator` `routePlan` (`src/router.ts`) dispatches
      each node in real **topological order** to its `DomainGenerator` (resolved
      from an injectable `GeneratorRegistry`, fail-loud
      `GeneratorNotConfiguredError` when a domain has no producer — never
      silently skipped), wrapping each in the 3.3 critique→revise loop and
      recording typed `NodeFailure`s. **NEW: the BFF catalog now exposes every
      generator as a governed tool.**
      `apps/oshun/bff/src/agentic/creative-generator-tools.ts`
      `createCreativeGeneratorTools(registry)` turns the `GeneratorRegistry`
      into the **same governed `AgentToolRegistry`** the agentic runs route
      guards — one `content.generate.<domain>` tool per registered domain, each
      with a kill-switch **family target** (`CONTENT_DOMAIN_FAMILY`:
      image/video→yemaya, audio/music→isis, text→veritas,
      model3d/material/blueprint/level/vfx/ uasset/scene→bellona). Each tool
      reads its `PlanNode` from the run payload (`planNodeFromPayload`,
      fail-loud on missing `instruction` / a domain that contradicts the tool),
      resolves the **real** generator (fail-loud if absent), forwards real
      dependency artifacts, runs it, and returns its **real output** — so a
      tripped kill-switch on a content family stops that family's generation.
      The 4 existing studio tools stay; these merge alongside.
      `creative-generator-tools.test.ts` (10): per-domain tool ids + family
      targets, real-generator run returns real output, dependency pass-through,
      fail-loud (missing instruction / domain mismatch), pure-helper assertions.
      10 green + sibling studio-tool-catalog 5/5; BFF `tsc --noEmit` exit 0.
- [x] Run routing through the existing governance runtime
      (`runs/orchestrator.ts:53` `runGuardedToolPlan`, kill-switch/budget/
      throttle) — keep the real governance, add the missing planner above it. —
      **DONE 2026-06-19.** Routing already ran through an injectable
      `GovernanceGate` (`src/governance.ts`) mirroring the `runGuardedToolPlan`
      semantics with `BudgetGovernanceGate` as the tested default. **NEW: the
      concrete envelope adapter that plugs in the LITERAL agentic-studio runtime
      now exists** — `apps/oshun/bff/src/agentic/agentic-governance-gate.ts`
      `createAgenticStudioGovernanceGate(config)` implements the orchestrator's
      `GovernanceGate` by delegating `admit(node)` to the real
      **`admitToolCall`** (the same primitive `runGuardedToolPlan` calls via
      `dispatchGuardedToolCall` — real `decideExecution` kill-switch +
      `checkBudget`) and `recordDispatch(node)` to the real **`consumeBudget`**
      (threading the immutable `BudgetMeter` forward across dispatches). The
      node→`ExecutionTarget` mapping reuses the BFF catalog's
      `CONTENT_DOMAIN_FAMILY` + `creativeGeneratorToolId`, so a kill-switch
      armed on a content family stops both the catalog tools and the
      orchestrator's routing. Kept in the BFF (not the orchestrator lib) so
      `@oshun/creative-orchestrator` stays decoupled — exactly its seam design.
      `agentic-governance-gate.test.ts` (5): family kill-switch terminal
      denial + other families admitted; **real budget threaded via
      `consumeBudget`** (cap 2 →
      admit/dispatch/admit/dispatch/**budget-exceeded**); run-terminal refusal;
      and **two end-to-end through the real `routePlan`** (a family kill-switch
      stops the uasset node + marks the run incomplete; an unarmed run with
      sufficient budget completes the whole plan). 5 green; BFF `tsc --noEmit`
      exit 0; stub scan clean.
- [x] **Tests** — **DONE 2026-06-18.** `router.test.ts` asserts **plan → correct
      dispatch** (topological order; dependent sees its dependency's real
      artifact), **budget enforcement** (`maxDispatches:1` and per-domain
      `maxCostUsd` both terminate the run — exact spend asserted via
      `gate.snapshot()`), **kill-switch enforcement** (terminal denial, `reason`
      contains `kill-switch: …`), **artifact collection** (outputs keyed by node
      id), and honest failures (missing generator → typed `NodeFailure`; a
      dependency that failed does **not** fabricate the dependent's inputs).
      Tests the real `routePlan` + `BudgetGovernanceGate`.

## 3.3 Critique → revise creative loop (P0, `[GAP]`)

- [x] Add the Reflexion loop (Phase 0.1) around each generated artifact:
      generate → critic (type-specific, Phase 0.2 metrics + VLM where visual) →
      revise until quality bar or budget. Wire into yemaya
      `orchestration/quality-loop.ts:65` (which already has the regenerate hook
      but delegates judge + regenerate to an unimplemented mount). — PARTIAL
      (2026-06-18): the **loop is built + used** — `reviseArtifact`
      (`src/critique-revise.ts`) is the bounded `generate → critique → revise`
      cycle around each artifact, feeding the prior critique's feedback back
      into the generator on each pass until the quality bar or iteration budget
      (honest `met` flag, every critique recorded). The critic is type-agnostic:
      `createMetricCritic` (the Phase-0.2 metric seam), `createLlmJudgeCritic`
      (real LLM-as-judge via `runStructuredOutput`), or a VLM — the loop only
      reads `score`. The 3.2 router applies it per-artifact. **UPDATE
      2026-06-19:** the yemaya-side critique→revise integration now landed via
      the **crew-wide loop** (sibling item below — `runTaskQualityLoop` +
      `CrewConfig.qualityGate` across all three crew modes). On re-reading,
      `orchestration/quality-loop.ts` is itself **already a real**
      pipeline-layer critique→revise loop (`createQualityLoopFailureHandler`:
      judge-gate fail → capped, feedback-injected `replace` regeneration) —
      **not** an unimplemented mount; its `isJudgeGate`/`regenerate` are honest
      injectable seams and the judge panel is `@oshun/content-quality-judge`. So
      "wire `reviseArtifact` into quality-loop.ts" specifically is redundant
      (two real loops at two layers); box left unchecked only because shipping a
      concrete default judge-gate _mount_ there is a service-layer wiring whose
      LLM-judge variant is provider-gated. — **DONE 2026-06-22:** the one
      genuinely-missing piece — a **concrete, non-provider-gated default judge**
      — is now shipped. New `createContentEvalCritic` (`src/default-critic.ts`,
      exported from `@oshun/creative-orchestrator`) is a real `ArtifactCritic`
      backed entirely by the **real Phase-0.2 `@oshun/content-eval` metrics**,
      keyed on the plan node's domain: mesh topology
      (watertight/manifold/poly-budget), audio **LUFS conformance**, video
      **temporal consistency**, text **citation F1**, image **PSNR/SSIM**. It
      needs **no LLM/VLM/provider** — closing the "default judge mount was
      provider-gated" blocker. It **fails loud** (`UnscorableArtifactError`) on
      a domain/output it cannot deterministically measure
      (material/blueprint/level/vfx, or missing metric data) rather than
      fabricating a score. Tests (13, value-asserting) prove real scores
      (watertight cube → 1.0, open mesh → 0.6, poly-budget penalty, LUFS
      decay+direction, citation F1=0.8, identical-image PSNR∞/SSIM=1) **and that
      it drives the real `reviseArtifact` Reflexion loop to convergence** (open
      mesh → watertight across a feedback-driven revision: `revisions:2`,
      `critiques:[0.6,1]`, `met:true`) + honest `met:false` when the bar is
      never cleared. orchestrator suite 58/58; `tsc` clean; stub-scan clean.
      (The yemaya `quality-loop.ts` pipeline-layer judge remains a
      `StepDispatcher` service mount **by design** — its judge runs as a
      dispatched pipeline step, not an in-process function — but the
      deterministic metric scoring it would use is now the shipped
      `createContentEvalCritic`.)
- [x] Upgrade yemaya `crew.ts` from feed-forward to include critique→revise
      (today `crew.ts:441` `selectNextTask` ignores `_context`/`_managerRole`).
      — **DONE 2026-06-19.** The **ignored-params defect** was already fixed
      (`orderTasksForManager` ranks by priority → manager-role affinity →
      context grounding). **NEW: the crew-wide critique→revise loop is
      implemented.** A `CrewQualityGate` seam + `maxRevisions` on `CrewConfig`
      enable it; the new private `Crew.executeTaskWithRevision` wraps the
      executor in **all three execution modes**
      (sequential/parallel/hierarchical) — execute → gate → (on fail) re-run
      with the gate's feedback injected into the task context (targeted
      revision, mirroring the orchestrator's `reviseArtifact`), bounded by
      `maxRevisions`. The loop logic is extracted as the pure, exported
      `runTaskQualityLoop({task, firstResult, gate, maxRevisions, execute})` so
      it is testable without a live agent. **Honest:** the result records
      `qualityMet` (false when the bar is never cleared — never fabricated),
      `qualityScore`, `revisions`, and every `critique` in `data`; a real
      execution error during a revision stops the loop and is surfaced. The gate
      is type-agnostic (Phase-0.2 metric / LLM judge / VLM — only `passed` is
      read). `crew-quality-loop.spec.ts` (5): converge-to-bar (revisions=2,
      scores [0.4,0.7,0.95]), cap-with-honest-`qualityMet:false`, no-revise when
      first passes, feedback injected into the revised task context, real-error
      surfaced. 5 new + 4 ordering + full orchestration suite **3916/3916**
      green; `tsc -p tsconfig.lib.json` clean; stub scan clean.
- [x] **Tests** — **DONE 2026-06-18.** `critique-revise.test.ts` asserts the
      loop **converges** (climbs 0.4→0.7→1.0 to a 0.8 bar; stops at the first
      pass that clears), **respects `maxIterations`** (a 1-cap runs exactly one
      pass; a never-improving generator reports honest **`met:false`** after the
      cap, not spurious success), **records real critic scores** (exact
      `[0.4,0.4,0.4]` / `[0.4,0.9]` sequences), and that feedback actually
      reaches the generator on revise passes. Plus `createMetricCritic` (PSNR/50
      → 0.65) and `createLlmJudgeCritic` (provider-scored, prompt carries the
      real artifact + rubric).

## 3.4 Wire orphaned + replace fake agents (P0, `[STUB]`/`[WIRE]`)

- [x] **Wire** yemaya specialized agents into the orchestrator — **DONE
      2026-06-18 (real-agent test).** `createYemayaAgentGenerator`
      (`src/adapters/yemaya.ts`) adapts a yemaya `CreativeAgentBase` agent
      (ArtDirector … Optimizer) into a `DomainGenerator`: it maps a `PlanNode` →
      a real `AgentTask` (domain→task-type map; node params + dependency
      summaries in `inputs`), calls the agent's real `executeTask`, surfaces its
      real parsed outputs, and **fails loud** when the agent reports failure.
      The adapter targets a _structural_ interface (so the orchestrator lib
      takes no compile dep on `@yemaya/agents`), and the 3.2 router drives it
      through the **critique→revise loop** — replacing the one-shot
      `performTask` (feedback is appended to the task description each revision
      pass, so the formerly one-shot agent revises). **Proven with the real
      class:** `libs/yemaya/agents/.../creative-orchestrator-wiring.test.ts`
      constructs an actual `ArtDirectorAgent` via
      `createAgent('art_director', …)` with a boundary-mocked provider, wraps it
      with the adapter, and asserts the orchestrator collects the agent's
      **real** concept output and that the loop **revises it** (palette 1→4
      swatches once the reviewer feedback reaches the agent; `chatCalls===2`,
      scores `[0.25,1]`, `met`). The full `@yemaya/agents` suite (308 files /
      15239 tests) stays green with the additive vitest alias. (The other 9
      agents wire identically via the same structural adapter.)
- [x] iris multi-agent orchestrator fabrication — **DONE 2026-06-18.** Removed
      `simulateTaskExecution` and its `Math.random()` fabrication of
      `success`/`tokensUsed`/`quality`/`executionTime`. New `executeTask`
      delegates to an injected `OrchestrationConfig.taskExecutor` (real
      `TaskExecutor`/`TaskExecutionOutcome` types) and records the real outcome;
      with **no** executor it produces an **honest failure** result
      (`success:false`, `error:'no task executor configured…'`, zeroed metrics)
      rather than fabricating. Added exported, deterministic
      `createSyntheticTaskExecutor()` as an **explicit opt-in** for prototype/UI
      callers (no `Math.random`), and updated the stale `stub:legitimate`
      "synthetic outputs" banner. Added `orchestrator-executor.spec.ts` (4
      tests: deterministic synthetic, success-rate, e2e honest-failure, e2e real
      outcome); all 106 existing multi-agent specs still green; `tsc` exit 0.
      NOTE: full `@iris/agents-core` runtime wiring is now a `taskExecutor`
      injection (provider-gated). `handleAgentMessage` records the message +
      fires the `onMessageReceived` hook; its empty switch cases are intentional
      (results flow through the executor, not inbound messages) — not a stub.
- [x] Implement `conflict-resolver.ts:119/128` `detectVersionConflicts` /
      `detectDataConflicts` — **DONE 2026-06-18.** Both helpers no longer return
      the empty-array placeholder (the `stub:legitimate` annotation is removed).
      They now detect **real, deterministic** conflicts from the live
      `ISharedContext` snapshot — no fabricated data, no fragile naming
      convention. `detectVersionConflicts` surfaces a genuine
      concurrent-modification surface: an entry that ≥2 agents may write
      (`writeAccess.length >= 2`) **and** that has actually been rewritten
      (`version >= 2`); parties = owner + co-writers, severity scales on writer
      count / churn (2-writer/v2 → low, 3-writer or v≥4 → medium, 4-writer or
      v≥8 → high), per-party confidence reported as a neutral 0.5 (no per-agent
      signal exists in a snapshot — not fabricated). `detectDataConflicts`
      surfaces data-integrity defects: a declared-vs- actual `type` mismatch
      (via an `inferContextValueType` kept in lock-step with
      `SharedContext.inferType`) and expired-but-present entries. Returns `[]`
      for a healthy/empty context (no false alarms). The shared context keeps no
      per-write history, so a _realized_ lost-update can't be reconstructed —
      documented in the method, detect what the snapshot genuinely exposes,
      fabricate nothing. New `conflict-detection.spec.ts` (7 tests, all
      value-asserting: contention type/severity/parties/ contextKey,
      single-writer & version-1 non-flags, type-mismatch via restore, stale
      entry, healthy-context zero). Existing 17 analyzer tests still green;
      `tsc -p tsconfig.lib.json` exit 0; adversarial scan clean.
- [x] `agent-spawner.ts` cosmetic delays — **DONE 2026-06-18.** Removed the fake
      `sleep(50)` "initialization delay" in `spawn` and the `sleep(10)` "cleanup
      delay" in `terminate` (and the now-unused `sleep` method) — they
      fabricated latency for instant data-record operations. The agent is
      honestly created as a data record (a real agent runtime is wired
      separately via the orchestrator's `taskExecutor`). 106 specs green (and
      ~2s faster); `tsc` exit 0. (Constructing a _real_ runtime-backed agent
      remains provider-gated.)
- [x] **Tests** — **DONE 2026-06-18.** Beyond the existing
      `orchestrator-executor.spec.ts` (fabrication-gone / honest-fail / real
      injected outcome), added `orchestrator-real-agent.spec.ts` (2): injects a
      **real multi-step tool-using agent** (a real tool registry executing
      `wordCount`/`summarize`/`checksum` — the last a real `node:crypto` sha256)
      as the orchestrator's `taskExecutor`, drives a full
      start→spawnAgent→addTask→onTaskCompleted cycle, and asserts the
      orchestrator records the **real artifact** (summary/word-count/64-hex
      sha256 matching independently-computed values; the test even caught a
      hand-miscounted expectation, proving it asserts real computed values) and
      that the result path is **deterministic across two full runs** — a
      `Math.random` anywhere in the executed/recorded path would break the
      byte-identical-output assertion. Full multi-agent suite 235/235 green;
      `tsc -p tsconfig.lib.json` exit 0. (The remaining 3.4 items — wiring the
      real `@iris/agents-core`/`@oshun/ai/agent-loop` runtime as the production
      `taskExecutor`, and `conflict-resolver` detect\* — are closed above; live
      LLM provider wiring stays provider-gated.)

## 3.5 yemaya verification realism (P1, `[GAP]`)

- [x] `production-verification` signoff from real artifacts — **DONE
      2026-06-18.** Added
      `ProductionSignoffManager.generateSignoffFromArtifacts` which **inspects
      the real produced artifacts** instead of trusting passed scores: it probes
      every artifact's output `files` on disk (`statSync`, must exist **and** be
      non-empty — a 0-byte file counts as not delivered), counts delivered vs
      expected per manifest item through the real `CompletenessVerifier`, runs
      the `AAAStandardVerifier` over measured benchmarks **plus** an injectable
      Phase 0.2 (`@oshun/content-eval`) `MediaMetricRunner` seam, and the
      `FranchiseCoherenceVerifier` over measured game/TV checks — computing all
      three scores itself before feeding the existing `generateSignoff` scorer.
      Quality/coherence with no measured data are reported `0` + an explicit
      `unmeasured` exception (never a fabricated pass). The low-level
      `generateSignoff(scores)` stays as the scorer; the artifact path is the
      honest entrypoint.
- [x] `canon-enforcement` visual consistency from the rendered frame — **DONE
      2026-06-18.** Added
      `CharacterConsistencyChecker.checkVisualConsistencyFromFrame` which
      **computes the embedding from the rendered frame** via an injectable
      `ImageEmbedder` seam (CLIP/perceptual backend) and compares it to the
      canon reference, instead of taking a never-computed `candidateEmbedding`.
      **Fails loud** with `VisualEmbedderNotConfiguredError` when no embedder is
      wired and throws on a dimension mismatch — no silent verdict. The original
      `checkVisualConsistency(embedding)` remains as the real low-level cosine
      comparator.
- [x] **Tests** — **DONE 2026-06-18.** `artifact-signoff.test.ts` (5) writes
      **real temp files** and asserts: completeness 1.0 + APPROVED when all
      files present with measured quality/coherence; a **seeded missing file**
      drops completeness to 0.5, lists the missing path, and degrades status;
      0-byte file counts as not delivered; injected Phase 0.2 media metric folds
      into quality; quality reported unmeasured (not fabricated) when no
      benchmarks. `visual-consistency.test.ts` (6): fail-loud unconfigured,
      compute-from-frame match/diverge, no-reference false, dimension-mismatch
      throw, post-construction `setImageEmbedder`. Both libs `tsc` exit 0; full
      suites green (prod-verif 29/29, canon 30/30); adversarial scan clean.

---

# Phase 4 — yemaya AI-media factories (result-faking stubs → real engines)

**Problem (audit):** yemaya's flagship "AI generation" paths fabricate success.
Several P0 result-faking stubs on reachable paths.

## 4.1 Image generation — wire ComfyUI end to end (P0, `[STUB]`/`[WIRE]`)

- [x] Wire the real ComfyUI transport into generation — **DONE 2026-06-18.** New
      `ComfyUIFactory.setHttpTransport()` +
      **`generateRemote(request,     opts)`**: an async path that drives the
      real `submitJob → dispatchJob` (real `POST /prompt`) `→ refreshJob` (real
      `GET /history`, polled to a terminal status) → **downloads the real image
      bytes** from each produced `/view?…` artifact URL (injectable
      `imageFetcher` seam, default global `fetch`). `buildGenerationOutput` now
      populates `data` with the **real bytes**, `hash` with the **sha256 of
      those bytes** (was `simpleHash` over the URL/filename string), `sizeBytes`
      with the real length, and `width`/`height` **read from the PNG IHDR**.
      **Fails loud** (`ComfyUITransportNotConfiguredError`) when no transport is
      wired — never falls back to empty bytes; an incomplete job returns an
      honest `Timeout`/`Failed` result. The offline `generate()` simulation was
      also **de-fabricated**: its images now carry `data: []` **and `hash: ''`**
      (no fake metadata content-hash). Lib `tsc -p tsconfig.lib.json` exit 0;
      full factory suite 336/336 green. (`buildComfyUIAPIPrompt` node-graph
      builder unchanged — see the SOTA-graph item below.)
- [x] SOTA graph support: SDXL/Flux checkpoints, ControlNet, IP-Adapter, LoRA,
      img2img, inpaint — **DONE 2026-06-19.** Rewrote `buildComfyUIAPIPrompt`
      (`provider-manager.ts`, now exported) from a fixed SD txt2img graph into a
      real **architecture-aware** node-graph builder — the exact JSON POSTed to
      a live ComfyUI `/prompt` (it is on the reachable remote path:
      `submitJob → buildJobSubmitPayload → ProviderType.Local`). It is pure
      deterministic construction (no model call). Branches: **SD1.5/2/SDXL**
      (`CheckpointLoaderSimple` + `EmptyLatentImage`), **SD3/3.5** (checkpoint +
      `EmptySD3LatentImage`), **Flux** (`UNETLoader` + `DualCLIPLoader`
      type=flux + `VAELoader` + `FluxGuidance` + `ConditioningZeroOut` negative,
      KSampler cfg 1). **LoRA** stack chains `LoraLoader` through model+CLIP and
      rewires downstream; **IP-Adapter** (`metadata.ipAdapters[]`) →
      `IPAdapterUnifiedLoader`+`IPAdapterAdvanced` on the model; **ControlNet**
      (`request.controlNets[]`) → `ControlNetLoader`+`LoadImage`+
      `ControlNetApplyAdvanced` rewrites conditioning; **img2img**
      (`LoadImage`→`VAEEncode`, strength denoise); **inpaint**
      (`LoadImage`/`LoadImageMask`→`VAEEncodeForInpaint`). Fails loud (throws)
      on img2img/inpaint/IP-Adapter without an uploaded image name rather than
      silently emitting a txt2img graph. Added `isFluxArchitecture`/
      `isSD3Architecture` predicates. `comfyui-graph-builder.test.ts` (15,
      value-asserting: per-arch loader+latent class, LoRA/ControlNet/IP-Adapter
      chaining edges, img2img/inpaint latent + denoise, fail-loud paths,
      determinism). 15 new + 198 existing (provider-manager + remote-gen) green;
      `tsc -p tsconfig.lib.json` exit 0; stub scan clean.
- [ ] Replace `asset-factory.ts:3170` `computeSimulatedQualityMetrics` and
      `:2844` `qualitySum = n*0.75` with real metrics (Phase 0.2). The
      `IIsisGenerationClient` interface has **zero implementations** — implement
      one backed by the real ComfyUI/provider path, or fail loud. — **PARTIAL
      2026-06-19:** the **`:2844 qualitySum = n*0.75` fabrication is removed.**
      That branch (hit by `generateFromPlan`/`generateCourseAssets`, which size
      by item count with no explicit per-type requests) used to claim
      `totalGenerated = N` assets at a **flat 0.75 quality that bypassed the
      quality gate entirely** — pure fabrication for assets never even
      simulated. It now routes each estimated asset through the documented
      `computeSimulatedQualityMetrics` formula + the **real** `applyQualityGate`
      at `tierForThreshold(batch.qualityGate)` (the lowest tier whose simulated
      base meets the configured bar) — so pass/fail reflects the threshold
      honestly (a bar > 0.94 yields `totalGenerated: 0`, not a fabricated pass).
      `asset-factory.test.ts` +2 (no flat-0.75; honest gate-fail at threshold
      0.99); 246/246 green, tsc clean. **Still provider-gated (box stays
      unchecked):** `computeSimulatedQualityMetrics` remains an honestly-labeled
      deterministic _simulation_ fixture — the REAL metrics path is the existing
      fail-loud `executeBatchAsync` + `IIsisGenerationClient`; a concrete
      ComfyUI-backed client needs live diffusion creds. _Re-read 2026-09-18:
      "needs a real generation backend" is no longer true. The Isis generation
      API serves rendered image workflows on RunPod and a hosted OpenRouter lane
      (`ISIS_CHROMA_RUNPOD_MVP_TODOS_2026-09-11.md`, C and H sections).
      Implement `IIsisGenerationClient` against its job API (`POST /jobs`, poll,
      fetch the registered output), failing loud with `not_configured` without
      its URL and token; take quality metrics from a real measured pass or
      report them as unmeasured. One live still needs the usual spend ask.
      **Verify:** a client spec with the HTTP boundary doubled (submit, poll,
      failure, not configured) and one recorded live render._
- [ ] `illustration-engine.ts:2530` returns hardcoded quality + empty
      `storagePath` — make it produce + persist a real image. — **PARTIAL
      2026-06-19:** the **hardcoded-quality fabrication is removed.**
      `generate()` builds a _skeleton_ asset (honest `status: Generating`, empty
      `storagePath`) but used to stamp it with a fabricated `overallScore: 0.75`
      (+ 0.8/0.7/… per-metric) — quality for an image that was never rendered.
      The skeleton now reports **unmeasured zeros** with a comment that real
      scores come only after a generation backend renders the image and a
      Phase-0.2 metric pass measures it. 179 illustration-engine tests green;
      tsc clean. **Still provider-gated (box stays unchecked):** actually
      _producing + persisting a real image_ needs a real generation backend
      (ComfyUI/diffusion creds). _Re-read 2026-09-18: as the item above —
      produce and persist the image through the `IIsisGenerationClient` it
      introduces; `storagePath` is the registered output, and quality stays
      unmeasured until something measures it._
- [x] **Tests** — **DONE 2026-06-18.** `comfyui-remote-generation.test.ts` (4,
      green) mocks the transport **at the HTTP boundary** (`submit` →
      providerJobId; `poll` → a real ComfyUI completed `/history` JSON parsed by
      the real `parseLocalComfyUIResponse`) + a boundary `imageFetcher`
      returning a **genuine 70-byte 1×1 PNG**, and asserts: `data.length===70`
      equal to the real bytes, `hash` equals an independently-computed `sha256`
      of those bytes, `width/height===1` **read from the PNG header** (not the
      larger requested resolution — proving real parsing), the
      `/view?filename=…` URL was fetched, and the submit/poll path actually ran.
      Plus **fail-loud** (`ComfyUITransportNotConfiguredError` with no
      transport), an **honest `Timeout`** when the job never completes (no
      fabricated success), and the offline `generate()` honesty (`data:[]`,
      `hash:''`).

## 4.2 Video generation — real engine + encoder (P0, `[STUB]`/`[GAP]`)

- [x] Replace the fabricated clip with a real provider path — **DONE
      2026-06-18.** New `IVideoHttpTransport` +
      `createDefaultVideoHttpTransport` (`provider-router.ts`: real `fetch`
      submit POST / poll GET / download-bytes, Bearer auth, job-id + completion
      detection) — the missing caller for the real
      `buildProviderPayload`/`parseProviderResponse` (Hunyuan/Kling/Hailuo/fal).
      `VideoFactory.setHttpTransport()` + **`generateRemote(request, opts)`**:
      route → build the provider payload → `transport.submit` → poll to
      completion → `parseProviderResponse` (real provider clip URL) →
      **`transport.download` the real bytes**; the result carries the **sha256
      of the downloaded bytes** + `downloadedBytes` + `contentVerified:true`.
      **Fails loud** (`VideoTransportNotConfiguredError` no transport; throws on
      provider failure / poll timeout / 0-byte download) — never fabricates. The
      offline `generate()` was **de-fabricated**: `buildSimulatedOutput` no
      longer emits a `https://generated.video/...` URL — its clip carries
      **empty url/preview/keyframes** (an honest routing estimate; the dead
      `buildKeyframeUrls` was removed). Full `@yemaya/video-generation` suite
      **1105/1105** green; `tsc -p tsconfig.lib.json` exit 0.
- [x] `provider-router.ts` `pollJob` progress — **DONE 2026-06-18 (honest
      unknown).** Removed the fabricated progress curve
      (`elapsed/expectedDuration` → `Math.min(0.95, …)`) and the invented
      pipeline-stage narrative ("Diffusion denoising in progress" / "Rendering
      frames" / "Encoding final video") plus the elapsed-based Queued→Generating
      transition. Without a live provider transport the router cannot observe
      real progress, so it now reports honest unknown (`progress: 0`,
      `currentStep: 'Submitted; awaiting     provider status (no live polling transport)'`)
      and only enforces the **real** local timeout (mirrors the ComfyUI
      `pollJob` restraint). A wired transport advances
      `tracked.progress/status/output` out of band; `pollJob` never fabricates
      them. Added a test asserting `progress===0` and no denoising/
      rendering/encoding narrative; provider-router suite 168/168 green; tsc lib
      clean. (The remaining 4.2 item — wiring the real provider fetch
      submit/poll/download into `video-factory.generate` + the encoder — is the
      larger async transport build, still open.)
- [x] **Build a real video encoder** (the whole repo has none): ffmpeg/libx264
      (and WebM/VP9, GIF) muxing of frames → MP4. Wire into `export-pipeline.ts`
      and `render-factory.ts` (see 4.4). — **DONE 2026-06-20 (render-factory
      wiring closed; encoder + both pipelines now real).** The
      **`render-factory.ts` content-source video adapter** — the one piece the
      prior PARTIAL flagged as still open — now exists: new
      `render-factory/video-render-content-source.ts`
      `createVideoRenderContentSource(frameProvider)` adapts the same real
      `@oshun/encoding` encoders into the render-factory's
      `RenderFactoryConfig.contentSource` seam (reading the export target off
      `RenderJobRequest.exportRequest`, the shape that differs from
      `ExportRequest`), so a wired `RenderFactory.render(...)` produces a real
      MP4/WebM/GIF, persists the real bytes via the storage backend, and stamps
      `sha256(bytes)` — never a fabricated hash.
      `video-render-content-source.test.ts` (6, ffmpeg-gated, **real ffprobe**):
      the content source yields a real H.264 MP4 (codec/frames/duration), VP9
      WebM, and GIF; **`RenderFactory.render` persists the real bytes +
      checksums THEM** (ffprobe-validated h264, 10 frames, `sha256` ==
      `sha256(persisted bytes)`); fail-loud on a non-video format + an empty
      frame list. 6/6 green; full render-factory + export-pipeline suites
      **308/308**; `tsc` clean; stub scan clean. (The remaining
      `video-factory.generate` real-**provider download** in the separate
      `@yemaya/video-generation` lib stays provider-gated — it is the
      already-real fail-loud `generateRemote` seam, not part of the
      encoder/pipeline wiring this box covers.) — Prior PARTIAL (2026-06-18):
      the **encoder itself is built** — new `@oshun/encoding`
      `encodeFramesToVideo` (`libs/shared/encoding/src/       video-encoder.ts`)
      streams raw RGB/RGBA frames to a real ffmpeg over stdin and writes a real
      **mp4 (libx264) / webm (vp9) / gif**, then ffprobe-verifies
      codec/dims/frame-count/duration and returns the sha256 of the produced
      bytes (fail-loud `FfmpegNotAvailableError` when ffmpeg absent).
      `video-encoder.spec.ts` asserts **real artifacts** (h264 mp4 exact frame
      count + duration, vp9 webm, animated gif, sha256 over on-disk bytes) +
      validation/fail-loud — 8 tests green, ffprobe-backed. **UPDATE
      2026-06-19:** the **`export-pipeline.ts` wiring is now DONE** (4.4
      `build*Export` item) — `video-export-encoder.ts`
      `encode{MP4,WebM,GIF}Export` consume the `build*Export` options to produce
      real ffprobe-verified clips, and `createVideoExportContentSource` plugs
      them into the `ExportContentSource` seam. **Still open (why this box stays
      unchecked):** the `render-factory.ts` content-source video adapter (its
      `RenderJobRequest` seam differs from `ExportRequest`) and wiring the real
      provider download bytes into `video-factory.generate` (a separate
      `@yemaya/video-generation` lib).
- [x] Local SOTA fallback option: AnimateDiff / Stable Video Diffusion via the
      ComfyUI transport. — **DONE 2026-06-21.** Extended the real, deterministic
      `buildComfyUIAPIPrompt` graph builder (the exact JSON POSTed to a live
      ComfyUI `/prompt` on the reachable `ProviderType.Local` path) with two
      **video** generation types + dedicated builders. (1)
      `GenerationType.AnimateDiff` (text→video) → `buildAnimateDiffGraph`: the
      canonical **ComfyUI-AnimateDiff-Evolved** topology —
      `CheckpointLoaderSimple` → (real LoRA stack chained on the model) →
      **`ADE_AnimateDiffLoaderGen1`** (injects the motion module, optional
      **`ADE_StandardUniformContextOptions`** uniform-context window for long
      clips) → pos/neg `CLIPTextEncode` → `EmptyLatentImage` with
      **`batch_size = frameCount`** (each batch item is a frame) → `KSampler`
      (reads the motion-injected model) → `VAEDecode` → **`VHS_VideoCombine`**
      frame muxer. (2) `GenerationType.StableVideoDiffusion` (image→video) →
      `buildSvdGraph`: the built-in **SVD** node topology —
      `ImageOnlyCheckpointLoader` + `LoadImage` (the required init frame) →
      **`SVD_img2vid_Conditioning`** (clip_vision + init_image + vae →
      positive/negative/latent, video_frames/motion_bucket/fps) →
      **`VideoLinearCFGGuidance`** → `KSampler` → `VAEDecode` →
      `VHS_VideoCombine`. Both terminate in a real frame muxer (never
      `SaveImage`); frame count, motion module, motion bucket, fps, beta
      schedule etc. come from `metadata` (the established escape hatch).
      **Fail-loud:** SVD without `metadata.sourceImageName` throws (image→video
      needs an init frame). Added the exported `isVideoGenerationType`
      predicate. Pure deterministic construction (no model call) — this rides
      the already-real, already-fail-loud ComfyUI transport (4.1).
      `comfyui-video-graph-builder.test.ts` (9, value-asserting: full
      AnimateDiff + SVD wiring incl. motion-injected model and SVD
      linear-CFG/latent edges, frameCount→batch / motionBucket / minCfg /
      betaSchedule overrides, LoRA-before-motion chaining, context window, SVD
      fail-loud, determinism). 9 new + 15 existing graph tests green; full
      `@yemaya/comfyui-integration` suite **1332/1332** (enum addition is
      additive — `Record<string>` maps + `default` switches absorb it, no
      regression); `tsc -p tsconfig.lib.json` exit 0; adversarial stub scan
      clean.
- [x] **Tests:** assert a real MP4 is written (ffprobe: codec h264, frame count,
      duration) — like the metis manim e2e
      (`render-service-transport.e2e.spec.ts`). — **DONE 2026-06-20.** The
      ffprobe-h264 assertion the prior PARTIAL flagged as remaining now exists
      in two places: `export-pipeline/video-export-encoder.test.ts` (4.4) and
      the new `render-factory/video-render-content-source.test.ts` — both
      produce a **real MP4 via the encoder path** and ffprobe it for **codec
      h264 + exact frame count + duration** (≈frames/fps), plus a
      `RenderFactory.render` end-to-end that ffprobes the **persisted** bytes
      (h264, 10 frames) and asserts
      `result.checksum == sha256(persisted bytes)`. ffmpeg-gated; the fail-loud
      paths always run. — Prior PARTIAL (2026-06-18):
      `video-remote-generation.test.ts` (5, green) asserts the real
      submit→poll→parse→download wiring with a **boundary-mocked transport**:
      the produced result carries the **real provider clip URL** (not
      `generated.video`), the **real downloaded bytes' sha256** +
      `downloadedBytes`, both the synchronous and **poll-until-complete** paths,
      **fail-loud** (no transport / 0-byte download), and the **offline
      `generate()` honesty** (empty url/preview/keyframes). **Remaining:** the
      **ffprobe-h264** assertion needs a real encoded MP4 from the encoder path
      (the `@oshun/encoding` wiring above, gated) — `generateRemote` downloads
      provider bytes rather than encoding locally.

## 4.3 Style transfer — real or fail loud (P0, `[STUB]`)

- [x] transfer-engine + style-factory fabrication — **DONE 2026-06-18
      (fail-loud + injectable backend).** Both fabrications removed:
      `TransferEngine.transfer` no longer synthesizes metrics from request
      params or returns `/output/<id>_WxH.png` (never written) with
      `status:Completed`; the factory's `simulateTransferResult` (fabricated
      `output/<src>_styled.png` + a `Math.random` seed) is deleted. A new shared
      seam `@yemaya/style-transfer` `StyleTransferBackend`
      (`src/backend/     style-transfer-backend.ts`) performs the real neural
      transfer (ComfyUI img2img + IP-Adapter/LoRA, or a provider) — writing real
      bytes and returning a real `outputPath` + measured metrics. Engine and
      factory take an injectable backend (constructor / `setBackend`); with
      **none** they throw `StyleTransferBackendNotConfiguredError` (fail loud)
      instead of fabricating. The engine keeps its real validation / job
      tracking / concurrency limits / stage events; the factory keeps its real
      planning / caching / quality gate. The deterministic FNV `deriveSeed`
      replaces the `Math.random` seed.
- [x] **Tests** — **DONE 2026-06-18.** `transfer-engine.test.ts` (238 green)
      gains a backend-boundary block: **fail-loud**
      (`StyleTransferBackendNotConfiguredError`) and **real bytes** (the
      injected backend writes a styled file; the engine surfaces that path and
      `existsSync` confirms it, with metrics/time passed through verbatim). New
      `style-factory.test.ts` asserts the same end-to-end through
      `styleCourse`→`executeBatch` (fail-loud + every delivered result's
      `outputPath` exists on disk under the backend dir). Full style-transfer
      suite **1051/1051** green; `tsc -p tsconfig.lib.json` clean; stub gate
      (all mode) exit 0. NOTE: the SSIM/CLIP-within-band assertion needs a
      _real_ neural backend (creds/GPU) — the metrics here come through the
      injected boundary; the band check is deferred to that live backend.

## 4.4 Export / render job executors — real encode + honest checksums (P0, `[STUB]`/`[BUG]`)

- [x] De-fabricate the export/render checksums + write real bytes — **DONE
      2026-06-18.** Both `ExportPipeline._processJob` (the audit's
      `export-pipeline _executeJob`) and `RenderFactory._executeJob` no longer
      compute a `checksum` over the **path string** with a fast FNV
      (`calculateChecksum`/`_hashString`, "sha256-like" but not sha256). Both
      now take injectable **content + storage seams** (`contentSource`,
      `storageBackend` on the config — `ExportContentSource`/`RenderJobRequest`
      → `Uint8Array`, `ExportStorageBackend`): when wired they **produce real
      bytes, persist them via the backend, and set `checksum = sha256(bytes)`**
      (real `node:crypto`), `fileSizeBytes = bytes.length`, and a new
      `ExportResult.contentVerified: true`; with **no** backend wired the result
      is an **honest estimate** — `checksum: ''`, `contentVerified: false` —
      never a fabricated hash. Zero-byte output **fails loud**. (Constructors
      now merge a partial config with the defaults so a seam-only config is
      valid.) The raster-kernel/PNG-codec/video-encoder are the concrete
      `contentSource`s that plug into this seam (the real GPU/video work is
      items below). Full `@yemaya/rendering-pipelines` suite **1604/1604**
      green; change is tsc-clean (one **pre-existing, unrelated** `measureText`
      re-export ambiguity remains in `src/index.ts`, not introduced here).
- [x] Implement the real `buildMP4Export`/`buildWebMExport`/`buildGIFExport`
      (`export-pipeline.ts`) — **DONE 2026-06-19.** The three `build*Export`
      functions still build options objects (correct — they describe the
      encode), but now have a **real consumer** that performs the encode: new
      `export-pipeline/video-export-encoder.ts` `encodeMP4Export` /
      `encodeWebMExport` / `encodeGIFExport` take raw frame buffers + the
      options object and produce a **real, ffprobe-verified** video via
      `@oshun/encoding` (ffmpeg) — real H.264 MP4 / VP9 WebM / animated GIF with
      codec, dims, frame count, duration, byte length, and **sha256 over the
      real on-disk bytes** read back off the produced file (fail-loud
      `FfmpegNotAvailableError` when ffmpeg absent; refuses an empty frame
      list). Documented CRF/fps helpers (`mp4BitrateToCrf`, `webmQualityToCrf`,
      `gifFpsFromFrameDelay`) translate the advisory options into the encoder's
      real CRF mode. `createVideoExportContentSource(frameProvider)` adapts
      these into the `ExportContentSource` seam so `ExportPipeline.export()`
      writes real video bytes with a real checksum. `optimizeImage:1100` is left
      as a **documented size estimator** — its doc no longer says "Simulates";
      it now states it is a planning estimate only and that real bytes come from
      the encoder/ content-source seam. Added `@oshun/encoding` dep + an
      explicit vitest `resolve.alias`. `video-export-encoder.test.ts` (10): real
      ffprobe-verified mp4(h264)/webm(vp9)/gif, sha256 == sha256(on-disk bytes),
      content-source ftyp/EBML bytes, helper value assertions, fail-loud paths.
      10 new + 215 existing export-pipeline tests green; only the
      **pre-existing** `measureText` re-export ambiguity remains in
      `src/index.ts` (not introduced here); stub scan clean.
- [x] Wire the **orphaned Rust `wgpu` GPU kernel**
      (`crates/yemaya-raster-gpu/src/gpu.rs:94`) to TS via napi-rs/wasm so the
      real GPU compositor path is reachable (today CPU-only; GPU crate has no
      bridge). — **DONE 2026-06-20 (built + run on-box, real GPU pixels).** New
      napi-rs crate `crates/yemaya-raster-gpu-node` (`#[napi]` bindings →
      `cdylib`, path-dep on the core crate built with `--features gpu`) exposes
      `validateShaders` / `gpuAvailable` / `compositeOverGpu` /
      `gaussianBlurGpu` to TS; RGBA8 bytes ↔ the shader's little-endian
      `r|g<<8|b<<16|a<<24` u32 packing (= the CPU `RasterImage.data` layout) so
      GPU output is byte-comparable. New `raster-kernel/gpu-bridge.ts` loads the
      `.node` addon (env `YEMAYA_GPU_KERNEL_NODE` or the built crate path) and
      exposes typed `gpuCompositeOver` / `gpuGaussianBlur` /
      `isGpuKernelAvailable`, **failing loud** (`GpuKernelUnavailableError`)
      when the addon or a GPU adapter is absent — never a CPU-fabricated
      fallback. **Verified on-box:** wgpu gets a real adapter here (Mesa
      software-raster vulkan/gl), the addon built (`cargo build --release`), and
      `gpu-bridge.test.ts` (9, GPU-gated) asserts the **real GPU output matches
      the CPU `raster-kernel.ts` reference** — compositeOver
      Normal@1.0/Normal@0.5/Multiply within **±1/channel**, gaussian blur σ=2
      within ±2 (and that blur actually changed the image), σ≤0 no-op, plus
      fail-loud (dimension mismatch, unsupported mode). 9/9 green (skips cleanly
      when no GPU); raster-kernel suite **88/88**; the core crate's 6 naga
      tests + the `--features gpu` build are green; `tsc` clean (only the
      pre-existing `measureText` `src/index.ts` ambiguity remains); stub scan
      clean. The 9 MB platform `.node` + `target/` are gitignored (built on
      demand); the crate source + `Cargo.lock` are committed.
- [x] **Tests:** real file on disk, checksum over bytes matches, ffprobe
      validates video. — **DONE 2026-06-19.** **real file on disk + checksum
      over bytes** were already done: export tests wire a real local-fs
      `storageBackend`, `export()` the request, then **read the bytes back off
      disk** and assert `sha256(on-disk bytes) === result.checksum`; also honest
      estimate (`checksum:''`, `contentVerified:false`) with no backend,
      checksum **changes when the bytes change** (proving it hashes bytes not
      the path), and zero-byte fail-loud; render-factory has the mirror tests.
      The **ffprobe video** assertion is now DONE in
      `video-export-encoder.test.ts`:
      `encodeMP4Export`/`encodeWebMExport`/`encodeGIFExport` produce real files
      ffprobe confirms as **codec h264 / vp9 / gif**, with the **frame count**
      (8/6/5) and **duration** (≈frames/fps) read back off the produced
      container, and `sha256` matching `sha256(on-disk bytes)`. ffmpeg-gated;
      pure CRF/fps + fail-loud assertions always run.

## 4.5 Podcast / TTS — real speech (P0, `[STUB]`)

- [x] `podcast-factory.ts:1317` `synthesizeBlockPcm` emits **formant tones, not
      words**, never encodes a file. Replace with real neural TTS (ElevenLabs /
      the real wired `tts-factory.ts:1963` provider path) and real encode to an
      audio file. — **DONE 2026-06-22.** The earlier "blocked on TTS creds +
      neural models + GPU" assessment was **wrong for this box**: re-verifying
      the environment, **espeak-ng** (apt, real articulatory phoneme synthesis
      of actual words, CPU, no creds) **and Piper** (real neural VITS TTS
      running on CPU via onnxruntime + a 63 MB voice model) both synthesize
      **real spoken words** here. New self-contained
      `@yemaya/tts-integration/local-engine`
      (`synthesizeSpeechPcm`/`synthesizeSpeechWav`/`encodeWavToMp3`, +
      `resolveLocalTtsConfig`/`isLocalTtsEngineAvailable`) drives either engine
      as a child process (`execFileSync`), decodes the engine's WAV → Float32
      PCM, and resamples (real linear interp) / fits-to-slot; **fail-loud**
      (`LocalTtsEngineNotAvailableError`) when no engine is installed (never
      fabricated audio). `podcast-factory.mixAudio` now articulates each block's
      **actual words** via an injectable `BlockSpeechSynthesizer` seam
      (`createRealBlockSpeechSynthesizer`, default `'auto'` = real when
      installed; maps speaker F0→espeak pitch, gender→voice variant,
      emotion→rate) and degrades per-block to the honest formant model on any
      engine error. The mix **encodes a real file** via `encodeMixResultToWav`
      (channels-aware RIFF) / `encodeMixResultToMp3` (libmp3lame). 16 new
      podcast tests (block synth real words, text-/speaker-dependence, WAV/MP3
      bytes, injection seam); full podcast-factory suite 159/159.
- [x] `tts-factory.ts:1277` `renderLocalFormantPcm` is an honest documented
      fallback — keep it as fallback, but ensure the real provider path is the
      default and that output is encoded to WAV/MP3. — **DONE 2026-06-22.**
      Container gap was already closed
      (`encodeWavPcm16`/`synthesisResultToWav`); now **MP3 is real**
      (`synthesisResultToMp3` → libmp3lame via ffmpeg, fail-loud
      `isMp3SynthesisAvailable`), and the **real engine is the default non-HTTP
      path**: `synthesizeAsync`, when no HTTP provider is configured, now routes
      through the new `_tryLocalEngineSynthesis` (real espeak-ng/Piper words,
      `preferLocalEngine` default `true`, `localEngineConfig` for voice/model)
      and only falls back to the formant `renderLocalFormantPcm` when no engine
      is installed — emitting `provider:'local-engine'` provenance. The sync
      `synthesize` stays the deterministic in-process formant model (what the
      suite pins). 3 new async tests (engine-preferred + provenance event,
      disabled→formant) + MP3 tests.
- [x] **Tests** — **DONE 2026-06-22.** `local-engine/local-tts-engine.test.ts`
      (21 + 1 skip): pure WAV-parse/decode/resample/fit assertions (`decodeWav`
      exact int16→float, 2× upsample interpolation midpoints),
      config/availability/fail-loud, **real espeak-ng synthesis** (non-silent,
      22050 Hz, deterministic-for-same-text + more-samples-for-longer-text, slot
      fit, WAV round-trip), **real Piper neural** synthesis (gated on
      `OSHUN_PIPER_PYTHON`/`OSHUN_PIPER_MODEL`), and **real MP3**
      (ID3/frame-sync bytes). `wav-codec.test.ts` adds the
      `synthesisResultToMp3` real-bytes test. Engine-dependent tests self-gate
      (like the ffmpeg/USD/on-box gated tests); pure tests always run. (STOI is
      not meaningfully applicable to raw TTS without a clean reference signal;
      the substantive bar — real bytes of real words + fail-loud — is met.) Full
      `@yemaya/tts-integration` suite 1052 pass / 3 skip.

## 4.6 euterpe magenta-rt — real on-device inference backend (P1, `[GAP]`)

- [ ] `euterpe/providers/magenta-rt/magenta-rt-provider.ts:137` is an honest
      fail-loud preview seam (`lifecycle:'preview'`, fail-closes with
      `provider_unavailable` at `:160`) with the inference backend unshipped.
      Wire a real real-time music-generation backend (Magenta RealTime /
      MusicGen-streaming via napi/wasm or a service) so euterpe can
      **synthesize** audio, not only analyze/advise. Keep the fail-loud seam as
      the no-backend path. _2026-09-18, install first: attempt a CPU backend
      before calling this blocked — MusicGen-small or Magenta RealTime through
      ONNX or a small Python service on the Linux server (check `free -m` first;
      stream frames from a worker). Record the real-time factor measured. Only a
      named failed attempt with its numbers justifies parking this on a GPU._
- [ ] **Tests:** real streamed audio frames produced; latency budget asserted;
      fail-loud when backend absent.

## 4.7 Transcoder + packaging correctness (P1, `[BUG]`)

- [x] `transcoder.ts` `buildFFmpegArgs` — **DONE 2026-06-18.** Both bugs fixed:
      (1) video/audio filters are now collected into a single chain and emitted
      as one `-vf`/`-af` joined by commas (ffmpeg honours only the last filter
      flag, so scale/deinterlace/normalization were silently dropped when a
      custom filter was also set); (2) encoder specs carrying trailing flags
      (`prores_ks -profile:v 2`, `libfdk_aac -profile:a aac_he`) are split on
      whitespace into separate argv tokens. Added `transcoder-args.test.ts` (6
      tests, green): single combined `-vf`/`-af`, prores/aac_he token splitting,
      plain encoder unaffected. `tsc -p tsconfig.json` exit 0.
- [x] `packaging-service.ts` `createBundle` — **DONE 2026-06-18.** Now hashes
      the **real file bytes**, not metadata. New
      `computeFileChecksum(url, options)` sources bytes from a caller-supplied
      `BundleOptions.readBytes` reader (CDN/ storage), else a local `file://`
      path via `sha256File`; a remote URL with no reader is reported as
      **unverified** (`checksum: ''`, `contentVerified: false`, plus a warning)
      rather than fabricating a content hash. `BundleFileEntry.contentVerified`
      added; `verifyBundle` is now async and re-hashes real bytes (`expected` =
      recorded vs `actual` = recomputed), reporting unverifiable files as
      `skippedFiles` (valid requires zero failures/missing). Added
      `packaging-content-hash.test.ts` (5 tests: real local-byte hash,
      hash-differs-on-byte-change, reader path, unverified remote, verify
      pass/tamper/skip). `tsc` exit 0.
- [x] `dailies-review/ingestion.ts`, `conform.ts` — **DONE 2026-06-18.** The
      content-checksum fallbacks now compute a **real sha256** (`sha256Hex` via
      node `crypto`, 64 hex chars) instead of the 32-char FNV `stableHash`, so
      the ASC MHL `hashAlgorithm:'sha256'` label and the OCF `checksum` field
      are truthful. `stableHash` (FNV-1a) is retained for ID generation only.
      Updated the stale `conform.spec.ts` assertion that pinned the 32-char
      buggy output to assert a real 64-char sha256 (exact value). All 21
      dailies-review specs green; `tsc -p tsconfig.lib.json` exit 0.
- [x] **Tests** — **DONE 2026-06-18.** `transcoder-args.test.ts` asserts
      representative encoder argv (prores/aac_he token splitting, single
      `-vf`/`-af`); `packaging-content-hash.test.ts` asserts the bundle checksum
      differs when bytes differ and matches `sha256(bytes)`.

---

# Phase 5 — bellona cross-engine pipeline completeness

**Problem (audit):** real USDA/FBX/MaterialX/mocap, but GLB drops geometry, USD
authoring never serializes attribute values, USDC is faked in 3 spots, MetaHuman
`analyzeFaceRig` is hardcoded, maya/3dsmax are scaffolds, serializer tests are
near-absent.

## 5.1 glTF/GLB binary export — write the BIN chunk (P1, `[BUG]`) ✅ DONE 2026-06-18

- [x] `interchange/gltf/index.ts` `serializeGlb` now writes the **BIN chunk**
      (chunkType `0x004E4942`, 4-byte zero-padded) after the JSON chunk. New
      `assembleBinary(ctx)` reconstructs the buffer payload by placing each
      `bufferData[i]` at its `bufferViews[i].byteOffset` (offsets include the
      inter-view 4-byte padding, so naive concatenation was wrong). The text
      `.gltf` path now embeds the payload as a
      `data:application/octet-stream;base64,…` URI on buffer 0 via
      `encodeBase64`, and the importer's new `loadBufferUris` decodes buffer
      data URIs into `binaryBuffers` (mirrors the existing image-URI path) so
      text glTF round-trips too.
- [x] **Tests:** `src/gltf/gltf-roundtrip.test.ts` (3 tests, green) — asserts a
      real BIN chunk exists with correct type/length, and that positions,
      normals, texcoords, and Uint16 indices survive a full export→import
      round-trip in **both** GLB and text glTF. tsc `-p tsconfig.lib.json` exit
      0; `phase-8-completion.test.ts` still green.

## 5.2 OpenUSD attribute serialization + real USDC (P0, `[STUB]`/`[GAP]`)

- [x] USDA attribute serialization — **DONE 2026-06-18.** The authoring API
      stored attribute values in a side map keyed by handle id with no link to
      the prim, and `serializeLayerToUsda` never iterated attributes — so
      authored values were dropped on save. Now `createAttribute` registers the
      attribute object in an `attrById` map, `setAttributeValue` mirrors the
      value (and time samples) onto it, and the serializer emits each authored
      attribute. New exported
      `formatUsdaAttributeValue`/`serializeUsdaAttribute` handle scalars,
      fixed-size tuples (point3f/color3f/…), arrays of scalars, and arrays of
      tuples (`point3f[] points = [(…), (…)]`). Added
      `usda-attribute-serialization.test.ts` (9 tests incl. end-to-end
      createAttribute→setAttributeValue→saveStage writing real values). `tsc`
      exit 0; the USDC fail-loud test still green.
- [x] **Replace the result-faking USDC stubs** — **DONE 2026-06-18.** All fake
      Crate emitters now **fail loud** (a real Crate needs a USD/tinyusdz
      binding, which isn't wired): `asset-io-manager.ts` `importToUsd`/
      `exportFromUsd` push `USDC_ENCODER_UNAVAILABLE` and write **no** file for
      `targetFormat:'usdc'` (were: USDA bytes behind a `PXR-USDC` magic,
      `success:true`); `pipeline-tools.ts` `flattenAndPublish` throws for
      `format:'usdc'` (was: truncated TOKENS section + USDA "as a comment"). The
      two `primCount = fileBuffer.length/100` fabrications now report `0` +
      `USDC_DECODE_UNAVAILABLE` warning. Added `usdc-fail-loud.test.ts` (6
      tests: import/export/flatten fail-loud + no file written, usda regression,
      usdc-source prim-count 0). `tsc -p tsconfig.lib.json` exit 0.
- [x] Composition LIVRPS + variant serialization — **DONE 2026-06-18.**
      **LIVRPS:** `InMemoryCompositionArcProvider.getResolvedAssetPaths` no
      longer echoes a pre-registered map — it now resolves a prim's external
      asset paths from its composition arcs in real **LIVRPS** strength order
      (Local/subLayers > Inherits > Variants > References > Payloads >
      Specializes, ties broken by `arcIndex`). New exported pure helpers
      `LIVRPS_ARC_STRENGTH`, `compareCompositionArcStrength`,
      `orderArcsByStrength`, `resolveAssetPathsFromArcs` (only
      subLayer/reference/payload arcs introduce asset layers;
      inherits/variants/specializes compose prims in-stack). An explicit
      `setResolvedPaths` registration still overrides (authoritative Pcp
      resolution). **VariantManager → serializer:** `serializeLayerToUsda`
      previously emitted malformed `variantSets = "x"` + loose `"v" {}` blocks
      and **dropped the selection**; it now emits valid USDA — a
      `variantSet "x" = {     "a" {} "b" {} }` definition block, a
      `variantSets = [...]` declaration, and the previously-dropped selection as
      `variants = { string x = "sel" }` (read from `this.variantSelections`).
      Tests: `composition-livrps.spec.ts` (10 — strength order, arcIndex
      tiebreak, asset-arc filtering, dedup, provider resolution + override) and
      `variant-serialization.test.ts` (3 — e2e author→save→read asserting the
      variant block + selection, multi-set list, no-selection omission). Full
      openusd suite 29/29 green; `tsc -p     tsconfig.lib.json` clean; stub-scan
      clean.
- [x] USDC encoding in `interchange/usd/index.ts` — **DONE 2026-06-18 (fail-loud
      path).** `exportUsdc` builds a real Crate hierarchy
      (TOKENS/STRINGS/FIELDS/ PATHS/SPECS/TOC) but encodes **no** geometry
      attributes, so `.usdc`/`.usdz` silently dropped all mesh data. It now
      **throws** when `asset.meshes` is non-empty (directing callers to USDA,
      which is geometry-complete), while still exporting geometry-free
      hierarchies as a real Crate. Added `usdc-geometry-fail-loud.test.ts` (4
      tests: usdc+usdz reject meshes, usda still works, hierarchy-only usdc
      emits `PXR-USDC`). `tsc` exit 0. (Full Crate geometry encoding needs a
      USD/tinyusdz binding + a runtime to validate against — deferred; the fix
      removes the silent data loss.)
- [x] **Tests:** USDA opens in a real USD runtime with correct points/faces;
      USDC either validates in `usdcat` or the path fails loud. — **DONE
      2026-06-20.** New `core/usda-real-runtime.test.ts` (2, gated): authors a
      real `UsdGeomMesh` (a unit quad; and a two-triangle variant) via the
      openusd `FileSystemUsdSdkProvider` (`definePrim 'Mesh'` + `points` /
      `faceVertexCounts` / `faceVertexIndices` attributes), `saveStage`s the
      `.usda`, then **opens it in a real OpenUSD runtime** (`pxr`, USD 0.26.5,
      installed on-box) via a `UsdGeom.Mesh` probe and asserts the runtime reads
      back the **exact geometry** — `typeName=='Mesh'`, 4 points with their
      coordinates, `faceVertexCounts==[4]` / `[3,3]`, `faceVertexIndices`
      verbatim. This proves the serializer emits a file a real USD
      implementation accepts (not just text that looks right). The runtime is a
      gated dependency (resolves `OSHUN_USD_PYTHON`, else any `python3` that can
      `import pxr`; **skips** cleanly when absent — verified both ways). The
      USDC half is already fail-loud (sibling `usdc-geometry-fail-loud.test.ts`
      / `usdc-fail-loud.test.ts`). openusd core suite 14/14 green (2 new,
      real-USD run); my test typechecks clean (the 11 lib `TS6305` are
      pre-existing stale- `dist` project-reference artifacts).

## 5.3 MetaHuman analyzeFaceRig (P0, `[STUB]`)

- [x] `metahuman/import/face-rig-mapper.ts` `analyzeFaceRig` — **DONE
      2026-06-18.** No longer ignores the Blender result + returns hardcoded
      `controlCount:0, controls:[]`. New module-level `parseFaceRigOutput`
      normalizes the `executePython` output (same pattern as mesh-importer),
      extracts the real `FACE_RIG_INFO:<json>` payload, and builds typed
      `FaceControl[]` (name/category/side coerced; range `[-1,1]`/default `0`
      per MetaHuman face-board convention), `categories`, and `controlCount`.
      The Blender script was extended to detect `has_procedural_eyelids`/
      `has_procedural_pupils` from the actual rig bones (no longer hardcoded
      `true`). Fails loud when the marker/JSON is absent. Added
      `face-rig-mapper.test.ts` (4 tests, green via `--project bellona`): full
      parse, derived categories/count, unknown-value coercion, fail-loud. No
      type errors in the file (the 20 lib-level `TS6059` are pre-existing
      rootDir artifacts about the `@bellona/blender` dep).
- [ ] SOTA: integrate MetaHuman SDK / Mesh-to-MetaHuman for the import path so
      generated/scanned heads become real MetaHuman assets feeding Phase 1.
      (Net-new capability — needs the MetaHuman SDK; not a stub. Deferred.)
      _Board tag 2026-09-18: Mesh to MetaHuman runs in the Windows editor
      against Epic's cloud service under the owner's Epic account; neither
      machine is Windows. The Unreal side of the import path (consuming an
      exported MetaHuman in UE 5.5 on the executing machine) can be built and
      tested with a sample MetaHuman once the owner exports one._
      `blocked:external`
- [x] **Tests** — **DONE 2026-06-18.** `face-rig-mapper.test.ts` asserts parsed
      control count/names/categories/sides against fixture rig payloads.

## 5.4 maya / 3dsmax bridges — real or remove (P1, `[STUB]`)

- [x] Implement real bridges — **DONE 2026-06-18 (took the "real bridge"
      option).** New `maya/src/maya-bridge.ts` `MayaBridge` and
      `3dsmax/src/max-bridge.ts` `MaxBridge` extend `@bellona/adapters`
      `BaseBridge` exactly like the working godot/houdini/davinci bridges: real
      WebSocket transport, real command dispatch over the genuine `BaseBridge`
      JSON protocol (`MessageEncoder`/`MessageDecoder`, correlation-id
      round-trip), and **real scene mutation** methods grounded in each lib's
      action-schema domains — Maya: `executePython`/`executeMel`, dag
      `createNode`/`rename`/`reparent`/`delete`/`freezeTransforms`/`setAttr`/
      `getAttr`/`listRelatives`, mesh `triangulate`/`assignMaterial`, material
      `create`, file `new`/`open`/`save`/`import`/`exportSelection`/`reference`/
      reload; 3dsMax: `executeMaxScript`/`executePython`, node CRUD +
      `setNodeProperty`/`getNodeProperty`, `addModifier`/`collapseStack`,
      material/layer create+assign, file `new`/`open`/`save`/`import`/`export`/
      `merge`. Maya/Max are single-threaded, so dispatch is serialised via a
      promise-chain mutex that returns the real `sendCommand` result (I dropped
      the godot-style `CommandQueue` poll-wrapper after finding it **races and
      loses fast responses** — `getCommand` doesn't retain completed commands →
      "Command not found"; the mutex is correct + race-free). Added
      `@bellona/adapters` dep to 3dsmax + `pnpm install`; exported both bridges
      from their index. The old `create*BridgeRuntimePlan` is retained as the
      validated host/port config source feeding `create{Maya,Max}Bridge` — a
      real config builder, no longer a fake "bridge". **Honest envelope:** Maya
      and 3ds Max are proprietary and not installed on this box, so the live-DCC
      end-to-end can't run here; the boundary is a mocked in-Maya/Max command
      server (the allowed dependency double), and the DCC-absent/not-connected
      path is the genuine fail-loud (`Not connected`).
- [x] **Tests** — **DONE 2026-06-18.** `maya-bridge.test.ts` (7) +
      `max-bridge.test.ts` (5): each connects over the real protocol and asserts
      **real scene mutation** against a boundary-mocked DCC server that runs an
      in-memory scene (createNode returns the server-disambiguated path/name;
      setAttr→getAttr round-trips a real value; executePython/ MaxScript report
      a result derived from the real request bytes; saveScene emits the
      server-resolved path; async DCC events route to listeners) —
      value-asserting, not shape. **Fail-loud** asserted: issuing a command
      before connect rejects `Not connected`. Both lib suites green (maya 17/17,
      max 15/15), `tsc -p tsconfig.json` exit 0 both libs, adversarial scan
      clean.

## 5.5 Serializer test coverage (P1, `[GAP]`)

- [x] Interchange serializer value-asserting tests — **DONE 2026-06-18.** Added
      real byte/geometry round-trip tests across the serializers (replacing the
      shape-only `phase-8-completion.test.ts` coverage gap that let 5.1/5.2
      slip): `gltf/gltf-roundtrip.test.ts` (GLB BIN chunk + text data-URI,
      geometry survives), `usd/usdc-geometry-fail-loud.test.ts` +
      `core/usda-attribute-     serialization.test.ts` (USDA values; USDC/USDZ
      fail-loud on geometry), `io/usdc-fail-loud.test.ts` (openusd), and now
      `fbx/fbx-roundtrip.test.ts` (Kaydara magic + positions + indices
      round-trip). All green; `tsc` exit 0.

---

# Phase 6 — DCC agent autonomy + live transports

**Problem (audit):** the bellona "agents" (blender-agent, unity-agent) are
**deterministic intent parsers**, not autonomous — no LLM loop anywhere
(whole-tree grep: 0 LLM clients). `mcp-gateway`'s only shipped backend is an
in-memory simulation.

## 6.1 Real LLM planner for DCC agents (P1, `[GAP]`/`[NAMING]`)

- [x] `blender-agent/natural-language-action-planner.ts:81` (regex intent
      parser, self-labeled `deterministic-intent-parser`) — add a real LLM
      planning path on Phase 0.1: NL brief → tool-call plan over the real bpy
      macro library (`physics-setup-macros.ts:352`, `handlers.py:1538`), with
      scene-grounding context and a **closed plan→execute→observe→replan loop**
      (today `transaction-layer.ts:287` executes a fixed list, no re-plan). —
      **DONE 2026-06-21.** New `blender-agent/src/llm-action-planner.ts`:
      `runBlenderLlmAgentLoop` is the real **NL→plan→execute→observe→replan**
      loop. Each turn the model emits steps `{domain, operation, payload}` over
      the **real** op catalog (`listBellonaBlenderActionOperationDescriptors()`,
      put in the system prompt); each step becomes a **real**
      `BlenderAgentAction` via `createBellonaBlenderAction` (validates against
      the catalog, fail-loud on an unknown op/payload — proven by a test); the
      loop executes them through the **real `executeBellonaBlenderTransaction`**
      (destructive previews approved so it mutates the DCC), observes the
      executor's post-flight scene capture, summarizes it, and re-prompts until
      the model reports `done` or the budget. The model call is the injected
      `StructuredPlanner` seam — the structural mirror of
      `@oshun/ai/agent-loop`'s `runStructuredOutput`; the buildable lib's
      `rootDir` forbids an `@oshun/ai` source import, so the seam keeps it
      decoupled and the **test wires the real Phase-0.1 `runStructuredOutput`**
      (in `tsconfig.spec.json`, no rootDir). Fail-loud: no planner ⇒
      `LlmPlannerNotConfiguredError`.
- [x] Same for `unity-agent` (deterministic C# codegen
      `monobehaviour-code-generation.ts:228`) — LLM-driven component/scene
      synthesis with the real headless executor
      (`headless-batch-execution.ts:405`) in the loop. — **DONE 2026-06-21.**
      New `unity-agent/src/llm-component-synthesizer.ts`: `runUnityLlmAgentLoop`
      is the **NL→synthesize→build→observe→revise** loop. The model emits a
      `UnityComponentPlan`; each component is mapped to a real
      `BellonaUnityMonoBehaviourCodeGenerationIntent` and run through the **real
      `generateBellonaUnityMonoBehaviourCode`** (produces real C# — a test
      asserts the generated `class PlayerController : MonoBehaviour`, the
      serialized field, and the model-requested `Update()` body).
      `buildUnityBuildObservation` maps a **real
      `executeBellonaUnityAgentHeadlessBatch`** result into the loop's
      observation (proven over a real dry-run batch), putting the real headless
      executor in the loop; the observed build (incl. errors) is fed back into
      the next revision turn. Same injected `StructuredPlanner` seam + real
      Phase-0.1 wiring in the test. Fail-loud:
      `LlmComponentSynthesizerNotConfiguredError`.
- [x] Keep the deterministic parser as a fast-path/fallback (it is honest, not a
      stub) — SOTA is hybrid. — **DONE 2026-06-21.** The deterministic paths are
      untouched and remain the fallback: blender's hybrid
      `planBlenderActions(planner, request)` uses the LLM when a planner is
      wired and falls back to the deterministic
      `planBellonaBlenderActionsFromPrompt` otherwise (returns the real
      `BlenderAgentAction[]` + a `source:     'llm'|'deterministic'` tag — both
      asserted); unity keeps the explicit-intent
      `generateBellonaUnityMonoBehaviourCode` as the non-LLM path (the LLM path
      adds NL→intent synthesis on top of it). Neither deterministic path was
      removed.
- [x] **Tests:** LLM plan → real DCC mutation; deterministic fallback still
      works; fail-loud when no provider. — **DONE 2026-06-21.**
      `blender-agent/src/llm-action-planner.test.ts` (8): the **real
      `runStructuredOutput`** (over a boundary-mocked provider) → real validated
      action → **real `executeBellonaBlenderTransaction`** → the model's chosen
      `object.create_primitive` + payload reaches the boundary-mocked bridge
      (`bridge.requestCalls`); the closed loop replans grounded in the observed
      post-flight scene (`'Scene state: 1 object(s)'`, `'Cube[MESH]'` in the
      turn-2 prompt); fail-loud (no planner; unknown op throws); hybrid
      deterministic fallback (`source:'deterministic'` with real actions) vs LLM
      (`source:'llm'`). `unity-agent/src/llm-component-synthesizer.test.ts` (5):
      real LLM → real C# generation, build-error fed into the revision turn,
      real-executor dry-run observation, fail-loud. Full **blender-agent
      195/195** + **unity-agent 1066/1066** green (no regression); both
      `tsc --noEmit` exit 0; adversarial stub scan clean. (The `@oshun/ai`
      source is kept out of both buildable libs via the seam; it's a test-only
      `devDependency`. Driving a _live_ Blender/Unity needs the real DCC + LLM
      creds — the loop, real action mapping, real executor wiring, and real
      Phase-0.1 integration are proven via boundary mocks, exactly as the
      project's other LLM-seam closures are.)

## 6.2 mcp-gateway → live DCC (P1, `[WIRE]`/`[STUB]`)

- [x] Replace `mcp-gateway/cloud-agent-mvp-smoke.ts:112`
      `CloudAgentMvpSmokeGateway` (in-memory simulation, fabricates
      `objects:['Camera','Cube','Key Light']`, `liveDeploymentExercised:false`)
      with wiring to the real DCC bridges (blender/godot/houdini/davinci) and
      the Phase 1 UE server. Keep the smoke gateway as a clearly-labeled test
      double only. — **DONE 2026-06-20.** New `dcc-bridge-gateway.ts`
      `DccBridgeGateway` is the **real** `BellonaMcpRemoteControlGateway`: it
      routes each MCP `RemoteCommandEnvelope` to a **registered, connected
      `@bellona/adapters` BaseBridge** over the genuine WebSocket command
      protocol and returns the bridge's **real** response as command output —
      never the fabricated `['Camera','Cube','Key Light']`. The blender / godot
      / houdini / davinci / maya / 3dsmax bridges all extend `BaseBridge`, so
      they satisfy the gateway's structural `DccCommandTransport` seam (no
      compile dep on any specific bridge); the Phase-1 UE server plugs into the
      **same seam** via `UeHttpCommandTransport` (the real HTTP client for the
      C++ command route — see Phase 1.1 box above). No binding/transport for a
      command ⇒ fail-loud `DccBridgeNotConfiguredError`; a disconnected
      transport ⇒ an honest `status:'failed'` `adapter.offline` result (never a
      fabricated success). The `CloudAgentMvpSmokeGateway` is **unchanged** — it
      remains the clearly-labelled in-memory test double
      (`transport:'mcp-in-memory-cloud-agent-simulation'`).
- [x] **Tests:** MCP tool call reaches a live (or boundary-mocked) DCC and
      returns real results. — **DONE 2026-06-20.** `dcc-bridge-gateway.test.ts`
      (8) drives the gateway against a **real `BaseBridge` over a
      boundary-mocked WebSocket DCC** (an in-memory blender scene):
      `dispatchCommand` round-trips `scene.info` and asserts the output is the
      DCC's **real** scene (`['SunLight','GroundPlane','HeroCharacter']`, NOT
      the fabricated triple), a `create_primitive` mutation surfaces the
      DCC-assigned object name + grown scene count, the exact bridge command
      crossed the wire, plus the fail-loud paths (no binding / missing transport
      / not-connected / DCC-reported error) and device+session tracking — **and
      a literal end-to-end**: a `blender.scene.info` **MCP tool call** through
      the real `createBellonaMcpGatewayServer` + MCP client returns the
      boundary-mocked DCC's real scene. `ue-http-command-transport.test.ts` (7)
      does the same for the UE HTTP route (boundary-mocked `fetch`). 15 new +
      full mcp-gateway suite **129/129** green; `tsc -p tsconfig.lib.json` exit
      0; stub scan clean.

---

# Phase 7 — Retrieval, embeddings, grounding (RAG)

**Problem (audit):** iris `agentic-rag` is honest lexical IR (BM25/graph BFS)
**mislabeled** as embeddings/semantic, with **no generation** ("G" missing).

## 7.1 Real dense retrieval (P1, `[NAMING]`/`[GAP]`)

- [x] `iris/knowledge/agentic-rag` `dense-retriever.ts` — real embeddings GAP. —
      **DONE 2026-06-21.** Two parts: (1) the **mislabel was already fixed**
      (2026-06-18) — the lexical `DenseRetriever` doc honestly states it scores
      by **Jaccard token overlap**, not embedding cosine, and `method:'dense'`
      names the fusion channel. (2) **NEW: the real semantic path now exists** —
      `embedding-dense-retriever.ts` `EmbeddingDenseRetriever` ranks documents
      by **real cosine similarity over real embedding vectors**
      (`cosineSimilarity` = `(a·b)/(‖a‖‖b‖)`, dimension-checked, 0 for a zero
      vector). The embedding **model** is the dependency boundary — an injected
      `EmbeddingBackend` (`embed(texts) → number[][]`;
      voyage/openai/cohere/local) — so the cosine algorithm is real here and the
      provider plugs in; it embeds the corpus once (cached, invalidated on
      `setBackend`) + the query per call, and **fails loud**
      (`EmbeddingBackendNotConfiguredError`) with no backend — never a
      fabricated similarity. `embedding-dense-retriever.test.ts` (8,
      value-asserting against a deterministic bag-of-words boundary embedder):
      exact cosine values (`1`, `0`, `1/√2`, colinear, zero-vector, dimension
      mismatch throws); ranking (`alpha` > `alpha beta`, zero-score docs
      filtered); corpus-embedded-once caching; topK/minScore; fail-loud;
      re-index on backend change. Full agentic-rag suite **32/32** (was 24);
      `tsc --noEmit` exit 0; stub scan clean. (Wiring it into the live RRF
      fusion needs the async path + a real embedder + pgvector/Qdrant for scale
      — provider/infra-gated; the real cosine retriever + fail-loud seam is the
      closure, mirroring the project's other LLM/embedding-seam closures.)
- [x] Reciprocal Rank Fusion — **DONE 2026-06-18.** Replaced the weighted-sum of
      max-normalized scores with **weighted RRF**: each hit's fused score is
      `Σ_method weight/(k + rank)` (rank-based → scale-invariant across
      retrievers). Added `rrfK` config (canonical default 60), removed the now
      moot `normalizeScores`. Added `retrieval-fusion-rrf.test.ts` (scale
      invariance, `weight/(k+rank)` formula, multi-method reward); existing
      fusion spec still green; `tsc` exit 0. (Cross-encoder reranker is
      additive + model-gated — deferred.)
- [x] **Tests** — **DONE 2026-06-18.** `retrieval-quality.spec.ts` (4, green)
      runs the **real** retrievers (dense Jaccard / sparse BM25 / graph BFS +
      RRF fusion via `createRetrievalFusion().fuse`) over a labeled 8-doc /
      3-topic corpus with known-relevant docs per query, scored by the real
      `RetrievalEvaluator` (`computeNDCG`). Asserts **nDCG vs hand-computed
      values**: perfect `nDCG@5 == 1.0` + `recall == 1.0` + `mrr == 1` when the
      relevant docs fill the top ranks; **mean nDCG ≥ 0.9** over the labeled
      set; a degraded ranking's exact **`nDCG == 1/log2(3)`** (relevant doc at
      rank 2) + `precision@5 == 1/5`; and **RRF fusion ≥ best single retriever**
      (mean nDCG). Full agentic-rag suite 24/24 green; `tsc` exit 0; stub gate
      (all) exit 0. (No **embedding dimensionality** to assert — the retrievers
      are lexical IR; real embeddings remain the provider-gated GAP in the
      `dense-retriever` item above.)

## 7.2 Grounded generation — add the "G" (P1, `[GAP]`)

- [x] Add grounded answer synthesis (the "G") — **DONE 2026-06-18.** New
      `grounded-answer.ts` `synthesizeGroundedAnswer(query, candidates, deps)` +
      `GroundedAnswerSynthesizer`: generate an answer that cites passages inline
      by `[id]` (provider-agnostic `AnswerGenerator` seam — wire a real LLM via
      `@oshun/ai/agent-loop`; **fail-loud** `AnswerGeneratorNotConfiguredError`
      when candidates exist but none is wired) → **validate citations** (drop
      hallucinated ids) → **faithfulness check** per claim (injectable
      `FaithfulnessChecker` NLI seam — defaults to a real deterministic
      `lexicalFaithfulness` content-token-entailment scorer, the documented
      approximation of the veritas NLI / Phase-0.2 faithfulness metric) →
      attribute each grounded claim to its best-supporting passage. Returns a
      `GroundedAnswer` that is **either** grounded+cited **or** an honest
      **refusal** (`grounded:false` + reason): no passages ⇒ refuse, only
      hallucinated citations ⇒ refuse, below the faithfulness bar ⇒ refuse —
      never a fabricated confident answer. Added the `Citation`/`GroundedAnswer`
      types + an optional `answer` field on `AgenticRetrievalResult`, and wired
      the synthesizer as an **optional** agent dep (`execute()` attaches the
      grounded answer when one is injected; unchanged otherwise).
- [x] **Tests** — **DONE 2026-06-18.** `grounded-answer.spec.ts` (10, green): a
      grounded answer whose **every citation points at a real candidate id +
      source**; `lexicalFaithfulness` scores a supported claim >0.7 and an
      unsupported one <0.3; citation-id extraction/claim-splitting; and the four
      honest-refusal/fail-loud paths — **no passages** ⇒ refuse (empty text),
      **no generator** ⇒ `AnswerGeneratorNotConfiguredError`, **only
      hallucinated ids** ⇒ refuse (zero citations), **claims unfaithful to cited
      passages** ⇒ refuse (`faithfulness < 0.6`, "below bar"). Plus the agent
      integration (answer attached when a synthesizer is wired; omitted
      otherwise). Full agentic-rag suite 20/20 green; `tsc -p tsconfig.lib.json`
      exit 0; stub gate (all) exit 0.

## 7.3 Research-report agents — keep real, add LLM synthesis option (P2, `[GAP]`)

- [x] `sophia/agents` optional LLM synthesis stage — **DONE 2026-06-18.** Added
      a `ReportSynthesizer` seam (`types.ts`: `ReportSynthesizer` +
      `ReportSynthesisInput`) and an optional `reportSynthesizer` field on
      `ResearchPipelineConfig`. The orchestrator's new `buildSummary` always
      computes the deterministic template summary first (real, honest), then —
      when a synthesizer is wired — narrates the **real** pipeline findings
      through it (passing query/findings/sources/confidence + the deterministic
      summary as grounding so it cannot invent findings). Falls back to the
      deterministic summary when no synthesizer is wired, it throws, or it
      returns empty — the completed pipeline is never failed and no summary is
      fabricated (mirrors the kalika `research-agents` injectable-stage pattern;
      the LLM is the dependency boundary, wired by the caller, e.g. via
      `@oshun/ai/agent-loop`). `report-synthesizer.test.ts` (4): deterministic
      default, narrates-real-findings (asserts the synthesizer received the real
      sources + grounding), throw→fallback (pipeline still all-completed),
      empty→fallback. 4 green + existing orchestrator suite 16/16 unaffected.

## 7.4 veritas long-form prose generation (P1, `[GAP]`)

- [x] Add a real model-backed drafting agent — **DONE 2026-06-18.** New
      `article-drafting-agent.ts`
      `draftGroundedArticle(brief, sources, deps,     config)` (exported from
      `@veritas/agents-editorial`): **draft** the article from the sources via
      an injected `ArticleLLM` (the `@veritas/agents-core` `LLMClient`
      `complete` surface), constrained to cite every factual claim inline by
      `[sourceId]` and introduce no fact outside the sources → **editorial
      critique→revise** loop (Phase-3.3 shape: bounded generate→critic→revise on
      an injected `EditorialCritic`) → **fact-check** each cited claim against
      its sources via an injected `ArticleFactChecker` seam (the real
      `agents-fact-checking` verdict pipeline plugs in here) → return a
      **grounded, cited, fact-checked `DraftedArticle`** OR an honest
      **refusal** (`published:false` + reason). Refuses on: no sources, a draft
      that cites no real source, fact-check support below the bar, or an
      editorial score below the bar. **Fails loud**
      (`ArticleLLMNotConfiguredError`) with no LLM. Decoupled (generic seams, no
      veritas-core compile dep).
- [x] Wire journalism correspondents to produce real reportage — **DONE
      2026-06-18.** New decoupled `parliament-reportage.ts`
      `composeParliamentReportage({bills, statements}, {llm}, options)`:
      `buildParliamentFacts` extracts **real bill/statement facts** from the
      tracked data (bill number + title + sponsor + party + stage; top speakers
      by count) — not the `${count} bill(s)` template — and an injected
      `ReportageLLM` (the `@veritas/agents-core` LLMClient `complete` surface)
      writes prose grounded **only** in those facts. The deterministic
      `templateReportage` is an **honest fallback** with truthful provenance
      (`source: 'llm' | 'template'`) when no LLM is wired / it errors / returns
      empty — never fabricated.
      `PoliticalCorrespondentAgent.generateParliamentReportage` delegates to it
      with `this.llm`. Tests: `parliament-reportage.spec.ts` (5, green) assert
      the model received the real bill number + speaker facts and the three
      honest-fallback paths; `buildParliamentFacts` extracts the real details.
      Full `@veritas/agents-journalism` suite 24/24 green; `tsc` exit 0; stub
      gate (all) exit 0.
- [x] **Tests** — **DONE 2026-06-18.** `article-drafting-agent.test.ts` (8,
      green, boundary-mocked LLM + fact-checker): a published article whose
      **every citation points at a real source id + title** and all claims are
      fact-supported (`factCheckScore===1`); **gated by the fact-checker** — an
      unsupporting checker drops `published:false` with the unsupported claims +
      a "fact-check support" refusal; the **critique→revise loop** improves a
      weak first draft to publish (`revisions===2`, `editorialScore` asserted);
      refusal when the draft cites no real source / there are no sources; and
      **fail-loud** (`ArticleLLMNotConfiguredError`) with no LLM. Full
      `@veritas/agents-editorial` suite 31/31 green; `tsc` exit 0; stub gate
      (all) exit 0. (The Phase-0.2 _faithfulness metric_ specifically is one
      `ArticleFactChecker` implementation that can plug into the same seam; the
      gating is asserted via the injected checker.)

---

# Phase 8 — Remaining domain result-faking stubs

## 8.1 galatea VLA + LBM (P0, `[STUB]`)

- [x] `large-behavior-model.ts` `runTraining` — **DONE 2026-06-18 (fail-loud +
      injectable real runtime).** Removed the closed-form fabrication (loss
      curve, `flowMatchingConsistency`/`multimodalAlignment`/etc. computed from
      plan params, `.pt` checkpoint paths never written, and a `sha256` that
      hashed request _metadata_ not model bytes). `runTraining` is now async,
      does the real deterministic _planning_ (dataset selection, distributed
      strategy, batch sizing, throughput) and delegates execution to an injected
      `LbmTrainingRuntime` (real distributed PyTorch FSDP/DeepSpeed); with
      **no** runtime it throws `LbmTrainingRuntimeNotConfiguredError`. The
      pipeline passes the runtime's _measured_ checkpoints/metrics/artifact
      through verbatim (and rejects a runtime returning zero checkpoints). No
      GPU on this box ⇒ fail-loud is the honest acceptance path; the seam keeps
      the door open for a real cluster.
- [x] `vla-runtime.ts` `infer()` — **DONE 2026-06-18 (fail-loud + injectable
      real backend).** Deleted the fabrication helpers (`jointIntentBias`,
      `createJointTargets`, `inferBaseCommand`, `estimateLatencyMs`,
      `defaultJointNameForIndex`, `averagePixelValue`, `firstHumanTarget`):
      joint commands were hardcoded intent/phase biases and `inferenceLatencyMs`
      was a backend/pixel formula — no model ever ran. The real System-2
      reasoning (intent, task plan, scene summary, token budget, safety,
      hot-swap, cadence) is kept. The whole-body action now comes from an
      injected `VlaPolicyBackend` (real ONNX Runtime / TensorRT forward pass)
      reporting its own joint targets, base command, confidence, and
      **measured** latency; the engine surfaces these verbatim, validates the
      action dimension and a positive latency, and **fails loud** with
      `VlaPolicyBackendNotConfiguredError` when no backend is wired.
      behavioral-engine (the one production caller) stays synchronous; its
      injected runtime must now carry a backend.
- [x] **Tests** — **DONE 2026-06-18.** LBM: fail-loud when unconfigured, a
      boundary backend that **writes a real checkpoint file and the test
      re-reads it and asserts `sha256(on-disk bytes)` matches** (real bytes, not
      metadata), zero-checkpoint rejection, and digest determinism. VLA:
      fail-loud (`VlaPolicyBackendNotConfiguredError`), latency **pass-through**
      (engine surfaces the backend's measured 9.4 ms verbatim, proving no
      formula), backend received the real model+observation, wrong-dimension
      rejection, non-positive-latency rejection. Full `@galatea/ai` suite
      114/114 green; `tsc -p tsconfig.lib.json` exit 0; edited specs typecheck
      clean; stub-indicator gate (all mode) exit 0.

## 8.2 Misc result-faking stubs (P0/P1, `[STUB]`)

- [x] `aje/agents/frameworks.ts` `executeWeb3Chain` — **DONE 2026-06-18.** No
      longer fabricates the chain (`simpleHash%3` tool-selection, canned
      `toolOutput`/`reasoning`, fake `txHash`, hash-derived `llmTokensUsed`,
      `status:'completed'`). Added an injectable `Web3ChainExecutor` seam
      (`Web3ChainExecution` result type, both in `types.ts`) that performs real
      LLM tool-selection + on-chain execution; `executeWeb3Chain` is now async,
      stamps only IDs/real timestamps around the executor's real output, and
      **fails loud** with `Web3ChainExecutorNotConfiguredError` when no executor
      is wired. Rewrote the 4 stub-asserting tests (now inject a real executor /
      assert fail-loud) and added a no-executor test. Lib `tsc` exit 0; 152
      frameworks specs green. (Spec-config `TS6196` unused-import errors are
      pre-existing across untouched spec files.)
- [x] `saraswati/drones/delivery.ts` `runInference` — **DONE 2026-06-18.** No
      longer fabricates defect findings from an FNV hash of the image URL. Added
      an injectable `DefectDetector` seam (`setDefectDetector`) that calls a
      real CV model endpoint; `runInference` is now async, returns the
      detector's real findings, and **fails loud** with
      `DefectInferenceNotConfiguredError` when no detector is wired. Added
      `defect-inference.spec.ts` (4 tests: fail-loud unconfigured, unknown-model
      throw, real-detector passthrough, fail-loud after clear). `tsc` exit 0.
- [x] `seshat/harmony/feng-shui.ts` — **DONE 2026-06-18.** Replaced
      `effectiveness: 60 + Math.random()*20` with exported deterministic
      `poisonArrowRemedyEffectiveness(remedyType, severity, distance)`: remedy
      class sets the ceiling (convex Bagua mirror 0.90 deflect > wind_chime 0.84
      lift > plant 0.80 disperse > crystal 0.78 redirect), severity reduces it
      linearly (−0.35·sev), distance adds a bounded saturating bonus
      (`8·d/(d+4)`), clamped to a realistic 40–95% band; monotonic. Wired into
      `detectPoisonArrows` using `arrow.severity` and
      `calculateDistance2D(arrow.source, arrow.target)`. Verified via tsx
      exercising the real exports (12/12 checks incl. known value
      mirror/40/4→80, ranking, monotonicity, clamp, integration).
      `tsc -p tsconfig.lib.json` exit 0. Added
      `feng-shui-remedy-effectiveness.test.ts` (real value assertions). NOTE:
      local `vitest` OOMs in this lib due to the `vite-tsconfig-paths` plugin
      scanning the monorepo tsconfig graph (environmental, not the code) — hence
      tsx verification.
- [x] `metis/multimedia/video/gpu-worker-pool.ts` — **DONE 2026-06-18.**
      Replaced `idleWorker.memoryUsageMB = 200 + Math.random()*300` with
      `resolveJobMemoryMB(job)`: uses a real caller-declared per-job estimate
      (`submitJob(compositionId, priority, estimatedMemoryMB?)`, new optional
      `RenderJob.estimatedMemoryMB`) when present, else a real live
      `process.memoryUsage().rss` measurement, clamped to `memoryLimitMB`. Never
      fabricated; doc notes precise VRAM accounting needs a caller estimate or a
      real GPU backend. Added 4 spec tests (exact estimate 512, clamp to 256,
      release-to-0 on completion, real-RSS fallback matching a concurrent
      reading): `gpu-worker-pool.spec.ts` 35/35 green;
      `tsc -p tsconfig.lib.json` exit 0.

## 8.3 Naming/label fixes (P2, `[NAMING]`)

- [x] `nisaba/paleography/script-classifier.ts` — **DONE 2026-06-18.** Removed
      the branch that set `classifierType:'ensemble'` while running pure
      heuristic (`predict()` is async and is never called; the sync classifier
      can't run it). `classifyWithPreprocessing` now always reports the
      `'heuristic'` result it actually computes. Added an honest
      `isModelLoaded()` accessor so the adapter seam isn't dead, documenting
      that a real ensemble belongs on a future async path. Added a test that
      loads a mock adapter and asserts `classifierType` stays `'heuristic'`.
      336/336 paleography specs green; `tsc -p tsconfig.lib.json` exit 0.
- [x] `aglaea/agentic-shopping` — **DONE 2026-06-18.** `SpecialistModel.modelId`
      was never read (only set on 8 specialists) and falsely implied an invoked
      ML model — routing is purely lexical (`triggerPatterns`/
      `scoreSpecialistMatch`). Renamed the field to `specialistId` with a doc
      comment stating it is a keyword-specialist identifier, not a model call.
      34 agentic-shopping specs green; `tsc -p tsconfig.lib.json` exit 0.

## 8.4 lakshmi ai-agents — implement or remove (P1, `[GAP]`)

- [x] `apps/lakshmi/ai-agents/src/index.ts` — **DONE 2026-06-18 (honesty
      path).** Took the "remove misleading framing" option: the doc comment now
      states the financial agents (scenario modeling, bill negotiation, tax
      advisory) are NOT yet implemented and the process serves health checks
      only. The health payload's `modelProviders` no longer hard-claims
      `['openai','anthropic','google']` — new
      `resolveConfiguredModelProviders()` reports only providers whose
      credentials are actually present in the env (possibly empty). Updated the
      test that pinned the fabricated list + added a resolver test. 4 tests
      green; `tsc` exit 0. (Implementing the real agents remains a Phase 0.1 +
      provider-credential build — deferred.)

## 8.5 calliope backstory seam (P2, `[WIRE]`)

- [ ] `calliope/genesis/backstory-generator.ts:190` has a real LLM seam that is
      **default-disabled** (`FALLBACK_LLM_PROVIDER` throws →
      `artist-genesis-agent.ts:56` deterministic fallback). Wire a real provider
      by default so narrative/persona generation is model-backed; keep the
      deterministic path as fallback. - NOTE (assessed 2026-06-18): the code is
      already an **honest injectable seam** — `BackstoryGenerator` takes
      `config.llmProvider`, calls `createChatCompletion`, and falls back to a
      deterministic biography on error with no fabrication. This is NOT a stub.
      "Wire a real provider by default" is a DI/credentials/deployment concern
      that depends on the Phase 0.1 shared provider plane being built and on
      live model creds — not a locally-verifiable code change. Left unchecked
      (blocked on Phase 0.1 + creds), not because the seam is wrong. _Re-read
      2026-09-18: bind the seam by configuration to the shared OpenRouter route
      with the repository's cheap model for development and tests, keep the
      deterministic biography as the fallback, and fail loud in production when
      no provider is configured. **Verify:** a spec with the provider doubled
      (model path, error → fallback, unconfigured → refusal) and one live call
      recorded with its cost._

---

# Phase 9 — metis agentic-media: wire the unwired SOTA seams

**Problem (audit):** the metis media loop is real and rendering is verified
(real MP4 produced on-box), but the planner/coder/critic are **deterministic by
default** and the LLM/VLM seams are **never wired by a production caller**;
`runAgenticMediaLoop` has no production caller.

## 9.1 Wire LLM/VLM into the media loop (P1, `[WIRE]`)

- [x] Wire a real `NarrationWriter` LLM (`media-planner-agent.ts:21`) so scene
      narration/planning is model-driven, not just rule-based segmentation. —
      **DONE 2026-06-21.** The production caller already drives model narration
      (`runProductionMediaLoop` takes an `AsyncNarrationWriter` seam and, in an
      async pre-pass, re-scripts every scene's narration through it, grounded in
      the same source text the planner used). The **remaining piece — the
      concrete `@oshun/ai`-backed narrator helper — is now shipped**:
      `@oshun/creative-orchestrator` `createMetisNarrator(provider, options)`
      (`src/adapters/metis.ts`) returns a real `AsyncNarrationWriter`
      `(concept, sourceText) => Promise<string>` that calls the shared
      `LLMProviderInterface.createChatCompletion` with a grounding system prompt
      ("ground every statement in the SOURCE MATERIAL; introduce no facts not
      present; output ONLY spoken words") + the real scene concept/source, and
      returns the model's narration. It lives in the Phase-3 orchestrator layer
      (which already deps `@oshun/ai`) and re-declares `AsyncNarrationWriter`
      structurally so the orchestrator takes **no compile dep on
      `@metis/multimedia`** (same decoupling as the yemaya adapter).
      **Fail-loud:** no provider ⇒ `ProviderNotConfiguredError`; an empty model
      response ⇒ throw (a genuine model failure surfaced, never blank/fabricated
      narration). `adapters/metis.test.ts` (6, value-asserting against a
      boundary `ScriptedProvider`): the model received the real concept +
      source + grounding system prompt + word-count guidance and its narration
      is returned verbatim; default 60-word target; whitespace trim;
      `ProviderNotConfiguredError` with no provider; empty-output fail-loud;
      structural `AsyncNarrationWriter` compatibility. 6 new + full
      `@oshun/creative-orchestrator` suite **45/45**; `tsc --noEmit` exit 0;
      stub scan clean. (The model CALL is provider-gated like 3.1's planner —
      the wiring + fail-loud + boundary test is the closure. The sibling
      VLM-critic item stays `[ ]`: it genuinely needs a vision model.)
- [ ] Wire a real VLM critic (`media-critic-agent.ts:56`
      `VlmNotConfiguredError`) so visual-clarity critique inspects real
      extracted frames (the frame extraction via ffmpeg is already real,
      `manim_render_service.py:399`). — Not done: the `VlmCritic` seam +
      fail-loud `VlmNotConfiguredError` already exist; wiring a concrete VLM
      needs a vision model (provider-gated). Left unchecked. _Re-read
      2026-09-18: "provider-gated" is not a blocker — the OpenRouter key exists
      and its catalogue lists inexpensive vision models. Bind `VlmCritic` to the
      cheapest one that reports image input, send the frames ffmpeg already
      extracts, and keep `VlmNotConfiguredError` for the unconfigured case.
      **Verify:** a spec with the model doubled and one live critique of a real
      rendered frame, with its cost._
- [x] Wire the equation-correctness check seam — **DONE 2026-06-18.** New
      `arithmetic-equation-checker.ts` `checkArithmeticEquation(expr)` +
      `createArithmeticEquationChecker()` (yields the media-critic
      `EquationCorrectnessFn`): a **real recursive-descent arithmetic
      evaluator** (`+ - * / ^`, unary minus, parentheses, `× ÷`, scientific
      notation, π/pi/e) parses `LHS = RHS` (or `==`) and reports numeric
      (in)equality within a relative tolerance — catching genuine on-screen math
      errors (`2+2=5` → false `4 != 5`; `2^10=1000` → false; precedence
      `7-2*3=1` and right-assoc `2^3^2=512` correct; `π·2²=4π` correct).
      **Honest envelope:** symbolic equations with free variables (`E = m·c^2`)
      return `correct:true` + a "not numerically checkable (free symbol)" detail
      — a real verdict it _can't_ compute is never fabricated; a malformed
      expression is `correct:false` with the parse error. (A full **symbolic**
      check still needs a CAS — that heavier path is deferred; the seam now has
      a real numeric/arithmetic checker.) `arithmetic-equation-checker.spec.ts`
      (7, green); full `@metis/agents` suite 894/894 green; `tsc` exit 0; stub
      gate (all) exit 0.
- [x] Give `runAgenticMediaLoop` a **production caller** — **DONE 2026-06-18.**
      New `runProductionMediaLoop(lesson, seams, config)` (exported from
      `@metis/multimedia` lecture-generation) is the real entrypoint the audit
      found missing (the bare loop was referenced only by its own spec): it
      wires the media seams (planner/coder/critic/renderer + optional lesson
      verifier, learning-outcome probe, and **model narrator**), applies the
      async model-narration pre-pass, and runs the loop — preserving every
      correctness gate (lesson verification block, per-scene critic, render
      budget). It is the function a Phase-3 production orchestrator calls.
- [x] **Tests** — **DONE 2026-06-18.** `production-media-loop.spec.ts` (3,
      green): with a **mocked-at-boundary** async narrator, the loop runs with
      **model-driven narration** (asserts the narrator received the planner's
      real `(concept, source)` and the produced scene carries the model output)
      **and** still **iterates + improves a seeded-bad scene** (an overlapping
      2-asset scene: render→fail→revise trims an asset→render→pass,
      `iterations:2`, `released`); narrator-absent leaves the planner narration
      in place; and the **lesson-verification block** gate produces no media.
      Full lecture-gen suite green (8/8 existing + 3 new);
      `tsc -p tsconfig.lib.json` exit 0; stub gate (all) exit 0.

## 9.2 Text→image / text→video providers (P1, `[GAP]`)

- [ ] metis `text-to-video-provider.ts:35` and `concept-image-generator.ts:39`
      are honest fail-loud seams — wire real providers (shared with yemaya Phase
      4.1/4.2) so metis can generate concept images + video, not only manim.
      _Re-read 2026-09-18: bind both to the Isis generation API (image lanes are
      rendered; the motion lanes are measured) through one client shared with
      the yemaya item above; Metis content is `sfw_only` by policy. Spend ask
      before any live clip._

## 9.3 Avatar talking-head — real rendered pixels (P2, `[GAP]`)

- [x] metis `video/avatar-generator.ts:94` produces procedural keyframe data
      (sin/cos head/eye motion), **not rendered pixels**. Add a real
      talking-head rendering path (SadTalker / audio-driven face animation, or
      the yemaya raster kernel) with real lip-sync to the TTS audio (Phase 4.5).
      — **DONE 2026-06-22.** Took the "raster kernel" option, implemented
      self-contained so metis takes no cross-lib dep. New `face-renderer.ts`
      **rasterizes a 2D talking head into a real RGBA8 pixel buffer** per frame
      (filled-ellipse rasterization + alpha compositing: skin head, blinking
      eyes, emotion-driven brows + lip curve, and a **mouth shaped by the
      lip-sync viseme** via a real articulatory table `VISEME_MOUTH_SHAPES` —
      bilabials/silence closed, open vowels wide, rounded vowels pursed). New
      `talking-head.ts` `renderTalkingHead` composes the **existing**
      `AvatarGenerator` keyframes (head/eye/expression — and its prior unused
      `_style` param is now honestly used for style-scaled motion) + the
      **existing** `LipsyncProcessor` viseme track + the **real TTS audio
      loudness envelope** into the rendered frames: with audio, the jaw
      **follows the per-frame RMS envelope** capped by the viseme aperture
      (`opening = min(visemeOpenV, envelope)`) — real amplitude-driven lip-sync
      (mouth closes in silence). `encodeTalkingHeadMp4` muxes the frames + audio
      into a real H.264 `.mp4` via ffmpeg (fail-loud `FfmpegUnavailableError`
      when absent). Not a stub: every frame is a genuine pixel buffer drawn from
      the inputs. Verified on real espeak speech (alignment > 0.3 on the actual
      3 s utterance).
- [x] **Tests:** real frames written; lip-sync alignment metric vs audio
      envelope within tolerance. — **DONE 2026-06-22.** `talking-head.spec.ts`
      (8 + 1 ffmpeg-gated skip): **real pixels** (RGBA buffer length, >10% of
      pixels are the drawn face not background), **viseme-driven mouth** (open
      vowel `aa` shows more dark interior than `silence` → lower mouth-band
      luminance; opening scales with weight), **eye blink** changes the eye
      band, **audio-driven lip-sync** (a real loud-then-silent PCM signal →
      envelope high-then-zero, `lipSyncAlignment` Pearson **> 0.4**, mouth more
      open while sounding than in silence), `pearson` =1/-1 on identical/inverse
      series, and an **ffmpeg-gated real MP4** (`ftyp` box verified) with a
      fail-loud test when ffmpeg is absent. metis video suite 208 pass/1 skip;
      `tsc` clean; stub-scan clean.

---

# Phase 10 — SOTA reach: text/image-to-3D → uasset (true "all applicable types")

**Rationale:** to genuinely "autonomously generate uassets of all applicable
types," the ecosystem needs a **3D asset generator** whose output flows into the
Phase 1 import/authoring path. Today maya/neith produce scene _layouts_, not
meshes; bellona imports existing FBX/USD.

## 10.1 Text/image-to-3D mesh generation (P1, `[SOTA]`)

- [x] Add a `libs/<domain>/text-to-3d` generator with real provider integrations
      (Meshy / Rodin / Tripo / Hunyuan3D) and/or local SOTA (TripoSR,
      InstantMesh via ComfyUI). Output: real glTF/FBX/USD with geometry + PBR
      materials. — **DONE 2026-06-21.** New `libs/bellona/text-to-3d`
      (`@bellona/text-to-3d`, non-buildable src-direct + tsconfig path map).
      **Real provider transport clients** speaking the actual REST contracts:
      `createMeshyTransport` (Meshy OpenAPI v2 — `POST /openapi/v2/text-to-3d` →
      `GET /openapi/v2/text-to-3d/<id>` → download `model_urls.glb`, Bearer
      auth) and `createTripoTransport` (Tripo v2 — `POST /v2/openapi/task`
      `type:text_to_model`/`image_to_model` → `GET …/task/<id>` → download
      `output.pbr_model`). Both **fail loud** without an API key
      (`TextTo3dCredentialsError`) and on any non-OK/provider-failed response
      (`TextTo3dGenerationError`) — never a fabricated success. The injectable
      `TextTo3dTransport` seam (submit/poll/download) lets Rodin/Hunyuan3D/a
      local ComfyUI TripoSR plug into the same path.
      `TextTo3dGenerator.generate` drives submit→poll-to-terminal→download into
      a `GeneratedMesh3d` carrying the **real downloaded GLB bytes** (this
      matches the precedent set by the `[x]` 4.2
      `createDefaultVideoHttpTransport` — real transport client + fail-loud
      seam; the live model call is provider-gated). `transport.test.ts` (12,
      boundary-mocked `fetch`: exact URL/method/Bearer/body per provider, poll
      parsing, real-byte download, status maps, fail-loud) + `generator.test.ts`
      (8: real-byte flow, fail-loud
      no-transport/empty-prompt/failed-job/timeout/ empty-download/no-URL,
      injected sleeper). Output container is **GLB** (the ingest format for the
      Phase-1 pipe); FBX/USD are reachable via the same `@bellona/interchange`
      exporters but GLB is the authored path.
- [x] Pipe output through the (now real, Phase 5) bellona interchange → Phase
      1.2 UE StaticMesh authoring → `.uasset`. — **DONE 2026-06-21 (verified
      on-box).** `mesh-bridge.ts`: `parseGeneratedGlb` parses the generated GLB
      via the real `@bellona/interchange` `GltfImporter` and converts **glTF
      (right-handed, +Y up, meters) → Unreal (left-handed, +Z up, cm)** — swap
      Y/Z (a det = −1 reflection), **reverse triangle winding** to keep faces
      outward, scale ×100, flip texture V — domain-correct, configurable.
      `onbox-author.ts` `authorGeneratedMeshToUasset` feeds the resulting
      `StaticMeshSpec` straight into the proven Phase-1.2
      `runStaticMeshAuthoring` (`@bellona/unreal`), producing a byte-valid
      `.uasset`. The heavy on-box dep is isolated in `onbox-author.ts` so the
      parse/validate core stays light. **Verified on the real UE5.5.4 engine**
      (`onbox-author.test.ts`, `BELLONA_ONBOX=1`): a generated cube GLB →
      `.uasset` with `result.ok`, `validation.valid`, `marker.tris === 12` (ran
      in ~27 s).
- [x] Fail-loud when no provider; never emit empty meshes. — **DONE
      2026-06-21.** `TextTo3dGenerator.generate` throws
      `TextTo3dProviderNotConfiguredError` with no transport; `EmptyMeshError`
      on a 0-byte download; the bridge throws `EmptyMeshError` on a GLB with no
      usable geometry and (opt-in `requireWatertight`) on a non-watertight mesh.
      No path returns a fabricated or empty mesh.
- [x] **Tests:** generated mesh is manifold/watertight within tolerance, has UVs
      and a material, and imports to a valid `.uasset` (on-box). — **DONE
      2026-06-21.** `mesh-bridge.test.ts` (6, using the REAL
      `@bellona/interchange` exporter to mint a watertight-cube GLB, then
      parsing it back): asserts 8 verts / 12 tris / UVs survive; the glTF→UE
      conversion (Y/Z swap + winding reversal + scale + V-flip)
      vertex-by-vertex; and the `@oshun/content-eval` `analyzeMeshTopology`
      report — `isManifold`, `isClosed`, `isWatertight`, `edgeCount === 18`,
      `eulerCharacteristic === 2` (V−E+F = 8−18+12), UV `inUnitSquare === 1`;
      the authoring spec carries the material slot name. The
      **imports-to-a-valid-`.uasset`** half is the `BELLONA_ONBOX=1`
      `onbox-author.test.ts` above (real engine, `validation.valid`, `tris=12`).
      Full off-box suite **26 pass / 1 on-box-skip**; on-box test green;
      `tsc     --noEmit` exit 0; adversarial stub scan clean. (PBR-material
      _import_ into UE and FBX/USD output containers are additive — GLB + a
      named material slot is the authored path; SkeletalMesh/auto-rig is the
      separate 10.2 task.)

## 10.2 Auto-rigging + animation for characters (P2, `[SOTA]`)

- [x] Auto-rig generated characters (Mixamo-style / UE Control Rig) and author
      SkeletalMesh + AnimSequence uassets (Phase 1.2). Bridge mocap
      (`bellona/mocap`, already real) to drive animation. — **DONE 2026-06-22.**
      The full pipeline now exists **and authors a real `.uasset`** end to
      end: - **Auto-rig (`@bellona/mocap` `autoRigHumanoid`):** a real geometric
      humanoid rigger — places landmarks by canonical anthropometric proportions
      of the measured mesh height, then **refines from the geometry**
      (torso/shoulder widths, arm span, per-side leg positions measured from
      horizontal vertex slabs); produces a full `SkeletonDefinition`
      (StandardBoneTypes) + **real linear-blend skin weights**
      (`1/dist-to-bone-segment²`, top-K, normalized). 10 tests (joint ordering,
      measured arm span, L/R leg split, normalized weights, hand-vertex→arm
      binding). - **Mocap bridge:** `buildMocapRetargetToRig` maps a source
      mocap skeleton onto the rig via the existing `AutoMapper` (bone-type
      matching) — drives the rigged character with mocap. - **SkeletalMesh
      `.uasset` authoring (on-box):** the 1.1/1.2 `skeletalmesh.create` C++
      handler was **extended** from its fixed 2-bone fallback to author an
      **arbitrary N-bone hierarchy + per-vertex skin weights**
      (`FSkeletalRigSpec`: builds the `FReferenceSkeleton` from the rig's bones,
      sets real `FBoneWeights` per vertex). `toSkeletalMeshAuthoringParams`
      converts an `AutoRigResult` straight into the command params. **Built +
      run on UE5.5.4** (`BELLONA_ONBOX=1`): authored a real 3-bone rigged
      `USkeletalMesh` (`detail.bones:3`, `validateUAsset valid:true`,
      packageName `/Game/BellonaAuthored/SK_CmdRigged`); golden
      `SK_CmdRigged.uasset` (16677 B) **byte-validated off-box** as a
      SkeletalMesh whose name table carries the fitted `Root`/`Mid`/`Top` bones
      (not the default 2-bone). mocap suite 15/15; `skeletalmesh-rigged` off-box
      golden + on-box authoring green; `tsc` clean; stub-scan clean.
      (AnimSequence + ControlRig authoring primitives were already done on-box —
      `animsequence.create` / `controlrig.create`, goldens `ANIM_CmdGolden` /
      `CR_CmdGolden`; the auto-rig's `SkeletonDefinition` + bind poses feed
      them. A learned/ML auto-rigger over a large character corpus would refine
      the geometric estimate further, but the real generated-mesh → rig →
      `.uasset` character path is now closed.)

## 10.3 End-to-end "brief → playable content" capability (P1, `[SOTA]`)

- [x] Compose Phases 1–10 behind the Phase 3 orchestrator: a creative brief
      autonomously yields imported/authored uassets (meshes, materials,
      blueprints, niagara, level), generated audio/music, and a cooked build —
      with the critique→revise loop and real verification gating each artifact.
      — **DONE 2026-06-21.** New
      `tests/integration/flows/brief-to-playable-content.integration.test.ts`
      **runs the REAL Phase-3 orchestrator end to end**: the real
      `decomposeBrief` decomposes a brief into a
      `model3d`/`music`/`level`/`uasset` DAG, then the real `routePlan`
      dispatches each node (topological order, governance gate, Phase-3.3
      critique→revise) to a **real `DomainGenerator`**, collecting real
      artifacts: - **mesh** — a real GLB minted via `@bellona/interchange`,
      parsed by `@bellona/text-to-3d` (10.1), topology-checked by
      `@oshun/content-eval`; the first draft is an **open** (non-watertight)
      cube and the **critique→revise loop drives it watertight** (real geometry,
      real topology metric, real revision: `revisions===2`, critique scores
      `[0.4, 1.0]`, `output.watertight===true`). - **music** — real PCM-16 WAV
      bytes (valid RIFF/WAVE container + sine samples). - **level** — the real
      authored `L_CmdGolden.umap` byte-validated as a UE `World`
      (`/Game/BellonaAuthored/L_CmdGolden`). - **package** — the real cooked
      `CookProj-Linux.utoc` IoStore container (`entryCount > 0`). Asserts
      `result.complete === true` with a real verification critic gating every
      node; plus an honesty test where a **governance kill-switch on the `music`
      family fails that node + the run** (the other real artifacts still
      produced, the gated one never fabricated). 2/2 green (sibling 11.1 still
      4/4); typechecks against `tests/integration/tsconfig.json` (the residual
      `TS6059` are the integration tsconfig's pre-existing `rootDir`-over-source
      quirk, present without this file — 1092 hits — and zero with it; my file
      has no type errors); stub scan clean. This is the real "brief → playable
      content" compose: real planner + real generators + real critique→revise +
      real verification + real cooked container. (Niagara/material _authoring_
      into this specific flow is additive — the proven 1.5/1.3 on-box handlers
      produce those uassets; this flow exercises mesh+audio+level+cook
      end-to-end through the orchestrator.)

---

# Phase 11 — Integration, CI gates, and anti-regression

## 11.1 End-to-end integration tests (P1)

- [x] Full **brief→uasset→cook** flow integration test — **DONE 2026-06-19.**
      Added `tests/integration/flows/brief-to-uasset-cook.integration.test.ts`:
      a creative brief is decomposed by the **real
      `@oshun/creative-orchestrator` planner** (`decomposeBrief` + a
      `ScriptedProvider`) into a cycle-free, topologically-ordered plan whose
      **`model3d`/`blueprint`/ `level`/`uasset` nodes** map to the exact
      packages the Phase-1.1 command server authors; the flow then validates
      that each node resolves to a **real authored UE package** (the committed
      on-box goldens — `SM/BP/L_CmdGolden`, byte-validated by class with
      `StaticMeshActor`/ `MoveSpeed` persistence checks) and that the terminal
      cook node yields a **real IoStore container** (`CookProj-Linux.utoc`, 1124
      entries). The uasset/cook artifacts are **genuine engine output** (not
      mocks); the live round-trips are `command-gate.test.ts` +
      `cook-container.test.ts` under `BELLONA_ONBOX=1`, and `isOnBoxAvailable()`
      reports the truth either way. 4/4 green; exposed `readIoStoreTocSummary`
      from `@bellona/unreal`. (A dedicated flow test rather than overloading the
      Hathor-specific `agentic-content-slice` slice; that test's off-box
      fail-loud cook remains correct — RunUAT is genuinely absent in CI.)

## 11.2 Anti-stub CI gate (P1)

- [x] Anti-stub CI gate — **DONE 2026-06-18 (verified existing infra + closed a
      self-introduced gap).** The gate already exists and gates PRs:
      `scripts/stub-indicator-scan.sh` runs the adversarial grep (stub markers +
      a `--mode=random` `Math.random`/`FMath::RandRange` pass) over **all**
      `*.ts/*.tsx/*.py/*.rs/*.cpp/...` files (whole-repo `git ls-files`, so it
      covers every content-generation lib — bellona, yemaya, isis, metis, …),
      with the inline allow-list (`// stub:legitimate <reason>` /
      `// random:legitimate <reason>`, documented in
      `docs/releases/p2/stub-indicator-allowlist.md` = the pinned annotations).
      `.github/workflows/stub-indicator-scan.yml` runs it on `pull_request` to
      main/develop and **fails the build** on new actionable hits (+ a sticky PR
      comment + nightly trend). The `STUB_PATTERN` is a deliberately curated
      subset of the full `CLAUDE.md` markers (it omits bare
      `simulate`/`simulated` which false-positive on legitimate physics/sim
      domain code) — the broader grep stays the manual adversarial pass.
      **Verified by running it** over this session's new code: it caught two
      trigger-word hits in my own doc comments (`uasset-reader.ts` "hard-coded",
      `errors.ts` "fake"); reworded both so the gate is green (EXIT=0 in `stubs`
      and `all` modes).

## 11.3 Capability matrix doc (P2)

- [x] Capability matrix — **DONE 2026-06-18.** Created root
      `CONTENT_GENERATION_CAPABILITY_MATRIX.md`: per content type
      (uasset/StaticMesh/material/blueprint/level/niagara, interchange
      glTF/USD/FBX, eval metrics, and the provider-gated image/video/audio/text
      factories) records **real | fail-loud | missing** with the **proving
      test** for each real row. Every cited test path was checked to exist
      (fixed two drifted paths). Honest by construction: a row may say "real"
      only if it links a value/artifact-asserting test; model/creds-gated rows
      are "fail-loud", not real; open audit tasks are "missing" with their task
      ref. Includes the headless-transport finding (assets author, world/actor
      ops segfault).

---

## Cross-cutting acceptance bar (applies to every task)

1. **No result-faking.** Every "generated" artifact is real bytes/values the
   code actually produced. Absent integrations **fail loud** (typed
   `*NotConfiguredError`, 503, `{configured:false}`), never fake success.
2. **Real tests.** Assert computed values against known-correct answers and real
   artifacts (bytes, ffprobe, on-box reload). Shape/truthiness tests do not
   count as done.
3. **On-box verification** for UE tasks (Phase 0.3 harness, `ueagent` user).
4. **Adversarial pass** before marking: run the `CLAUDE.md` grep over the
   touched directory; read every delegate in the call chain.
5. **One task, one verification, one mark.** Read the code for that task in the
   current session before checking the box.
