Disciplines · Audits

Agentic Content Autonomy — Gap-Closure TODOs (SOTA)

checkbox [ ]/[x] is the sole source of truth.

56sections121 minread

On this page

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 decomposeBriefroutePlan 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: UStateTreeEditorDataAddSubTree/AddTask/AddTransitionFStateTreeCompiler::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.modelIdspecialistId.
  • 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.
  • Provider-agnostic tool-use loop primitiveDONE 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.
  • 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).
  • 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.
  • 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.
  • TestsDONE 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])#

  • Create libs/shared/content-evalDONE 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.
  • 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.
  • TestsDONE 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.

  • On-box round-trip automation — DONE 2026-06-18 (verified on-box). Added libs/bellona/unreal/onbox/ (a Blueprint-only BellonaOnBoxProbe.uprojectDisableEnginePluginsByDefault + 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_assets 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.
  • 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.
  • 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-onlyheadless-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])#

  • 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.
  • .uplugin CanContainContent: true + an authoring editor module — DONE 2026-06-18. Flipped CanContainContent to true; the editor module now authors real assets and SavePackages them (BellonaCommandDispatcher.cpp builds a UStaticMesh from an FMeshDescription via BuildFromMeshDescriptions and saves a real .uasset).
  • 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.)
  • 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.)
  • 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])#

  • 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_descriptioncreate_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_assets 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.
  • 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.
  • 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 weightsGetVertexSkinWeights().Set(v, FBoneWeights::Create({FBoneWeight(0,1)}))), then runs the engine's own FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromMeshDescriptions (the real LOD build), SetSkeleton + MergeAllBonesToBoneTree, and SavePackages 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), RegisterMorphTargets 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.
  • TestsDONE 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])#

  • 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 / Emissivelayout_material_expressionsrecompile_materialsave_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).
  • 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.
  • 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 pipelineUInterchangeManager::CreateSourceData + the synchronous ImportAsset (bIsAutomated=true; the MaterialX translator/pipeline self-register from the plugin) → a UMaterial, then SavePackages 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).
  • TestsDONE 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.

  • 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 SavePackages. 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.)
  • 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).
  • TestsDONE 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])#

  • 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 SavePackages; 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.
  • 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).
  • 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).

  • level.create — author a .umap with placed actorsDONE 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 SavePackages a real .umap (GetMapPackageExtension). Verified on-box (no crash): authored /Game/BellonaAuthored/L_CmdLevel11418-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.)
  • 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 SpawnActors + 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.
  • 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 stalelevel.create spawns + persists actors on-box (the [x] level-authoring item), so only the TS placement→buffer glue remained, now built + tested.
  • 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])#

  • 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}), AddRows each (defaults to two sample rows), and SavePackages. 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.
  • 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 WAVBuildMonoPcm16Wav 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>SetSoundWaveFirstNodeLinkGraphNodesFromSoundNodes) 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.
  • 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.createUBlendSpace with a 3-sample triangle in the default [0,100] axes referencing the authored AnimSequence. • aimoffset.createUAimOffsetBlendSpace with additive (AAT_RotationOffsetMeshSpace) samples — the type the aim offset's IsValidAdditiveType requires (found + fixed on-box; AAT_LocalSpaceBase was rejected). • animmontage.createUAnimMontage 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.
  • 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 SavePackages. 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.)
  • Tests: per type, reload + assert structural correctnessDONE 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 validateUAssets 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])#

  • 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 validateUAssetd 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.)
  • 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 chowned 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])#

  • 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.
  • 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.
  • 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-boxV6DistrictPcgScatterTests.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.)
  • 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])#

  • Accept neith/maya scene-program output and Phase 1.2 mesh authoring to populate V6/ue/Content/Districts with real generated .uassets 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 .umaps are saved.)

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

  • 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.)
  • 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])#

  • 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).
  • 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.)
  • 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.
  • 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.
  • 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])#

  • 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.
  • Fix the mislabelDONE 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.)
  • TestsDONE 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])#

  • 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 NodeFailures. 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.
  • 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 existsapps/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.
  • TestsDONE 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])#

  • 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 + usedreviseArtifact (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.)
  • 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.
  • TestsDONE 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])#

  • 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.)
  • 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.
  • Implement conflict-resolver.ts:119/128 detectVersionConflicts / detectDataConflictsDONE 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.
  • 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.)
  • TestsDONE 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])#

  • 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.
  • 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.
  • TestsDONE 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])#

  • 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.)
  • 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 (LoadImageVAEEncode, strength denoise); inpaint (LoadImage/LoadImageMaskVAEEncodeForInpaint). 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.
  • TestsDONE 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])#

  • 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.
  • provider-router.ts pollJob progress — DONE 2026-06-18 (honest unknown). Removed the fabricated progress curve (elapsed/expectedDurationMath.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.)
  • 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).
  • 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 CLIPTextEncodeEmptyLatentImage with batch_size = frameCount (each batch item is a frame) → KSampler (reads the motion-injected model) → VAEDecodeVHS_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) → VideoLinearCFGGuidanceKSamplerVAEDecodeVHS_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.
  • 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])#

  • 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.
  • TestsDONE 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 styleCourseexecuteBatch (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])#

  • 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/RenderJobRequestUint8Array, 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 estimatechecksum: '', 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 contentSources 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).
  • 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.
  • 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.
  • 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])#

  • 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.
  • 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.
  • TestsDONE 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])#

  • transcoder.ts buildFFmpegArgsDONE 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.
  • packaging-service.ts createBundleDONE 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.
  • dailies-review/ingestion.ts, conform.tsDONE 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.
  • TestsDONE 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#

  • 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.
  • 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])#

  • 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.
  • Replace the result-faking USDC stubsDONE 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.
  • 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.
  • USDC encoding in interchange/usd/index.tsDONE 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.)
  • 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), saveStages 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 geometrytypeName=='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])#

  • metahuman/import/face-rig-mapper.ts analyzeFaceRigDONE 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
  • TestsDONE 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])#

  • 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 responsesgetCommand 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).
  • TestsDONE 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])#

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

  • 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.
  • 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.
  • 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.
  • 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])#

  • 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').
  • 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])#

  • 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 existsembedding-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.)
  • 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.)
  • TestsDONE 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])#

  • 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).
  • TestsDONE 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 generatorAnswerGeneratorNotConfiguredError, 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])#

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

  • 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).
  • 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.
  • TestsDONE 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])#

  • large-behavior-model.ts runTrainingDONE 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.
  • 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.
  • TestsDONE 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])#

  • aje/agents/frameworks.ts executeWeb3ChainDONE 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.)
  • saraswati/drones/delivery.ts runInferenceDONE 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.
  • seshat/harmony/feng-shui.tsDONE 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.
  • metis/multimedia/video/gpu-worker-pool.tsDONE 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])#

  • nisaba/paleography/script-classifier.tsDONE 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.
  • aglaea/agentic-shoppingDONE 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])#

  • apps/lakshmi/ai-agents/src/index.tsDONE 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 seamBackstoryGenerator 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])#

  • 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.
  • 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.
  • Give runAgenticMediaLoop a production callerDONE 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.
  • TestsDONE 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])#

  • 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).
  • 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])#

  • 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-3dGET /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_modelGET …/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.
  • 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).
  • 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.
  • 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])#

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

  • 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)#

  • 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)#

  • 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)#

  • 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.