01 · Overview

Two surfaces, one pipeline

In-app chat (electron/agent/sdkRunner.ts spawns the child) and the headless CLI (npm run agent) both run the same packages/autolens-agent/src/pluginRunner.ts — a Claude Agent SDK query() with the autolens-builder plugin, allowlisted ls-clad skills, and approved learnings injected, driving 25 in-process tools plus the live Lens Studio MCP catalog over http://localhost:<port>/mcp (port + token read from the project's .mcp.json).

Renderer (src/) React three-pane cockpit — Projects | Chat | Figma
↓ preload (window.api)
Main process (electron/) IPC handlers, event bus + SQLite, spawns the SDK child per run
↓ spawns per run
plugin-runner child (packages/autolens-agent) Claude Agent SDK query() + 25 in-process tools
↓ HTTP MCP (localhost:<port>)
Lens Studio MCP server the live editor — the user starts it, AutoLens never launches it

ARCHITECTURE.md:11-49, pluginRunner.ts:223-235

Core principle: the model plans; code executes and verifies. Every step that can be deterministic is a tool, not a prompt.

Routing gate — decided before Phase 0

The pipeline drawn on this page is the Snapchat / Figma branch: Figma ingestion → 2D screen-space compiler → verify, building a Snap Lens in the camera. Specs/Spectacles is a different branch — it needs SIK/UI-Kit/world-space 3D and is built by CLAD's specs-experience-builder instead; this pipeline stops rather than mis-build it. Precedence for deciding which branch: user-stated → project .esproj targetPlatform → ask. Never guess.

lens-experience-builder.md:76-92


02 · Provenance

Who owns what — three layers

The single most clarifying frame for the owner: who built what. Three stacked layers, Lens Studio at the foundation, AutoLens on top.

Layer 3 — AutoLens

AutoLens

The moat built specifically for this tool: everything that makes AutoLens more than "an agent poking Lens Studio."

  • 25 deterministic in-process tools (packages/autolens-agent/src/tools/)
  • 9 own skills (plugins/autolens-builder/skills/)
  • 6 own sub-agents (plugins/autolens-builder/agents/)
  • codegen — the layout/wiring/behavior compiler (src/lib/build/screenFlow/codegen.ts)
  • 18 Hard Rules (lens-experience-builder.md)

codegen and the Hard Rules are AutoLens's own — invented here, for this pipeline.

packages/autolens-agent/src/tools/, plugins/autolens-builder/skills/, plugins/autolens-builder/agents/, src/lib/build/screenFlow/codegen.ts, lens-experience-builder.md

↓ borrows read-only knowledge (never modifies)

Layer 2 — CLAD (ls-clad)

CLAD

Snap's vendored agent-knowledge library — a read-only snapshot at plugins/clad/: ~58 skills + 9 agents covering both Snapchat lenses and Spectacles. AutoLens borrows only 8 of its skills — the LS-general craft it would otherwise re-derive. It never modifies CLAD. The rest of CLAD (the entire specs-* family, the specs-experience-builder agent) is the Spectacles branch this pipeline's routing gate deliberately hands off, not builds.

plugins/clad/skills/ (~58 dirs), plugins/clad/agents/, skillAllowlist.ts:46-55

↓ drives over HTTP MCP

Layer 1 — Lens Studio (Snap)

Lens Studio

The raw editor, reached over MCP. The ~21 mcp__lens-studio__* tools are Lens Studio's own API surface, exposed by the LS MCP server the user starts. AutoLens calls them over http://localhost:<port>/mcp but does not own or ship them. They do primitive operations — create a scene object, run editor JS, generate a texture, recompile TypeScript, capture a preview — and they fail silently ("MCP success ≠ done": a write can return ok and change nothing). That silent-failure surface is the entire reason Layer 3 exists.

pluginRunner.ts:220,223-235, lens-experience-builder.md:17-61

The key relationship: an AutoLens-own tool is a deterministic compiler/verifier that orchestrates many Lens Studio MCP calls into one checked operation. build_from_manifest compiles a plan into a single VirtualScene apply instead of ~300 imperative LS calls; verify_geometry reads LS's scene-graphql and auto-corrects anchors; wire_screen_flow runs codegen + binds @inputs + recompiles + verifies in one call. The skills (own + CLAD) are the knowledge the orchestrator reads; the Hard Rules are the discipline it follows; the AutoLens tools are the deterministic hands it acts with; the LS MCP catalog is the raw editor underneath.

ARCHITECTURE.md:33-49, pluginRunner.ts:207-235


03 · Pipeline

The build pipeline, phase by phase

Routing gate → Phase 0 Plan → Phase 1 Assets → Phase 2 Build + Wire → Phase 3 Self-audit → Phase 4 Verify → Learn. Click a phase to expand it. HR chips link to the Hard Rules section; skill chips are the domain knowledge loaded at that gate.

Routing gateDecide the platform once, before any scene work — never interleave branches

What

A single, committed decision: is this the Snapchat/Figma branch (this pipeline) or the Specs/Spectacles branch (a different pipeline, built by CLAD's specs-experience-builder)?

How

Precedence, checked in order: user-stated platformproject .esproj targetPlatformask. Never guess.

Why

This pipeline is a 2D screen-space compiler only. Forcing a Specs/Spectacles request (SIK/UI-Kit/world-space 3D) onto it produces a wrong result — the gate stops rather than mis-builds.

lens-experience-builder.md:76-92

Phase 0 — Plan onceBuildManifest + Lens Brief, before any scene work

What

A validated understanding of the experience (Lens Brief) + a concrete per-scene build plan (BuildManifest).

How

  1. 0a — scan_figma: structural scan
  2. 0b — capture_figma_storyboard + read the overview render with vision + plan_lens_from_storyboard (persists .autolens/lens-brief.json; a plan-critique pass; unanswerable open questions PAUSE the run — they are not guessed)
  3. 0.5 — deploy_template: MUST return ok:true before any build; writes template-uuids.json
  4. 0c — probe the scene via scene-graphql
  5. 0d — emit the BuildManifest JSON

Why

"Plan once, execute declaratively" is the whole reliability thesis — improvising the next MCP call is the 300-call anti-pattern this agent exists to kill.

Hard Rules HR1 HR6 HR11 HR12 HR7 HR16

lens-experience-builder.md:100-425

Phase 1 — Asset ingestionEvery texture/font the design needs, installed into the project

What

Every texture/font the design needs, installed into the LS project, returned as a textureMap.

How

materialize_assets with the manifest assets + fontFamilies (resolves fonts via FontSelector; omitting them ships text as font:None). apply_element_animation records entrance-animation hints into the Brief.

Why

Assets are the moat — everything comes from Figma or bundled product assets, never a generator (the one exception is the lens publish-icon).

Skills loaded assets figma-import
Hard Rules HR10

lens-experience-builder.md:429-454

Phase 2 — VirtualScene execute + behavior wiringThe scene built (geometry) and the interactive behavior wired (scripts)

What

The scene built (geometry) and the interactive behavior wired (scripts).

How

  1. Per scene: build_figma_frame (the mandated Figma-frame builder — routes Safe/Full by geometry, remaps anchors, reuses logo, dedupes) — or build_from_manifest for residual hand-built layers — collapsing to ≤2 VirtualScene applies (Phase A create + Phase B @input wiring)
  2. Step 2e — wire_screen_flow generates the behavior layer via codegen (named feature controllers + the hub, binds @inputs)
  3. lens-script-author fills the //TODO(agent) stubs
  4. lens-verifier reviews them

Delegation is mandatory for 3+ scenes — spawn scene-builder per scene, sequentially.

Why

One declarative apply is diffable/idempotent/verifiable; hand-driving hundreds of calls is expensive and high-variance.

Hard Rules HR2 HR3 HR4 HR10 HR14 HR15 HR16

lens-experience-builder.md:458-508

Phase 3 — Self-audit + semantic gateProof the scene actually contains what the manifest said

What

Proof the scene actually contains what the manifest said.

How

A read-only scene-graphql grep (manifest ↔ scene, by name) + verify_semantics (checks meaning, not just existence — wrong-type rig @inputs, silent ts-input failures, missing layer masks, distorted aspect, dead textures).

Why

"The tool returned ok" is not "done" — something can exist but not mean what the wiring assumes; a live human caught exactly this class on the 07-01 run.

Hard Rules HR5 HR8 HR13

lens-experience-builder.md:512-558

Phase 4 — Verify loop (see-and-fix)Proof the scene looks right, is on the right camera, at the right depth, and behaves right

What

Proof the scene looks right, is on the right camera, at the right depth, and behaves right.

How

  1. verify_geometry — deterministic sizing (MANDATORY, run first, never by eye)
  2. verify_render_order — z-order
  3. verify_layer — camera-layer mask
  4. verify_lens_states — drive each state, capture, vision-diff vs Figma
  5. dispatch_verify_fixes — batch the mechanical fixes, escalate the rest
  6. verify_behavior — inject a gesture per interaction, read stateNow back — proves the tap landed
  7. capture_preview_sequence + verify_motion — grade the feel of transitions

The suite is keyed to the feature set — gate selection is not discretionary.

Why

Structure + stills both passed while the interaction was dead — nothing had actually tapped the lens; these gates close that.

Hard Rules HR9 HR17 HR18 HR13 HR7 HR16

lens-experience-builder.md:561-723

LearnDurable lessons captured at run-end and post-run, fed back into future builds

What

Durable lessons captured and fed back.

How

record_learning files a lesson at run-end (→ pending/); npm run harvest -- <archive> mines the run's NDJSON for the same signals; the owner approves/rejects in the Learning Hub; approved lessons inject into every future build prompt (cap 60). sweep_unused_assets is explicit housekeeping — never part of a build.

Why

The loop turns a live surprise into a permanent rule — but stale lessons mislead, so retirement tombstones them.

lens-experience-builder.md:727-729, learning.ts:20-41


04 · Data Shapes

What the agent sees — data shapes

The owner's question, answered literally: when the agent scans Figma, plans a lens, builds a scene, and wires it up, what JSON does it actually get back? Nine cards below walk down the pipeline in call order. Every example carries a real file:line type citation, plus a mandatory honesty badge: real fixture means the values come from an on-disk fixture/cache (mostly the Visa×FIFA Fan Type 1 scan and the asset-dedup texture UUIDs); constructed from type means the shape is real but the example values were built strictly from the TypeScript interface because no on-disk sample exists (no real lens-brief.json, facts.json, or BuildManifest exists anywhere in this tree). A constructed example is never presented as real.

plans/pipeline-explainer-site.md — Appendix H

scan_figmaThe lead card — structural scan of one Figma frame, real fixture values

What the agent does with this: reads each frames[].elements[].class to decide per element (bake PNG / reuse template logo / bundled hint / native Text) and uses anchorHint as the exact box the build will place — before writing the BuildManifest.

export interface FigmaScanElementItem {
  nodeId: string; name: string;
  bbox: {x: number; y: number; width: number; height: number};
  class: string; //export-png | text-layer | separate-animated | separate-interactive
                 //| reuse-template-logo | use-bundled-hint | skip-* | container
  exportable: boolean;
  anchorHint: {left: number; right: number; bottom: number; top: number};
  action: string;
  text?: string; fontFamily?: string; fontSize?: number; textColor?: string;
  renderOrder?: number; stretchMode?: number;
}
export interface FigmaScanFrame {
  nodeId: string; name: string; page?: string;
  lensMode?: 'selfie' | 'world' | 'common';
  bbox: {width: number; height: number};
  elements: FigmaScanElementItem[];
  recommendations: string[]; nextSteps: string[];
}
export interface FigmaScan {
  fileKey: string; frames: FigmaScanFrame[];
  devNotes: {page: string; text: string}[];
  detectedMode?: 'common' | 'selfie' | 'world';
  flow?: FigmaProtoFlow;
}

src/lib/figma/scanTypes.ts:3-19,21-30,43-50 — tool wraps as {ok, scan, errors} at scanFigma.ts:202; class union at figmaApiImporter.ts:411

real fixture hybrid — see note below
{ "ok": true, "scan": {
  "fileKey": "EXAMPLE_FILE_KEY", "detectedMode": "selfie",
  "frames": [ { "nodeId": "146:308", "name": "Fan Type 1", "bbox": {"width":334,"height":732},
    "elements": [
      {"nodeId":"146:311","name":"Type of Fan","class":"export-png","exportable":true,
       "action":"Bake to PNG → ImageLayer","bbox":{"x":76,"y":334,"width":182,"height":44},
       "anchorHint":{"left":-0.442,"right":0.4129,"bottom":-0.09,"top":0.03}},
      {"nodeId":"146:314","name":"Catchphrase","class":"text-layer","exportable":false,
       "action":"TEXT node → native Text","text":"You, my friend, are a","fontFamily":"Inter","fontSize":20,
       "anchorHint":{"left":-0.76,"right":0.76,"bottom":0.10,"top":0.18}},
      {"nodeId":"146:2","name":"Logo","class":"reuse-template-logo","exportable":false,
       "action":"Reuse template Logo SO (do not export)"},
      {"nodeId":"146:320","name":"Sparkle 1","class":"separate-animated","exportable":true,
       "action":"Separate PNG, will animate"}
    ], "recommendations":[], "nextSteps":[] } ],
  "devNotes": [], "persistentElementNames": ["Logo"] }, "errors": [] }
//… frames[] trimmed 11→1, elements trimmed to 4

Real: frame 146:308 "Fan Type 1" 334×732, element names/classes, and "Type of Fan"'s anchor (left -0.442 right 0.4129) + box are asserted verbatim in src/lib/figma/scanFigmaFrame.test.ts:118-136. Not real: the other elements' node ids/anchors are constructed from type.

capture_figma_storyboardThe whole-page render, for captions and flow the structural scan can't see

What the agent does with this: Reads overviewImagePath (the whole PAGE render — captions + flow arrows between frames the structural scan can't see).

return reply({
  ok: true,
  overviewImagePath,
  frameRenders,
  errors,
  note: overviewImagePath
    ? 'overviewImagePath is the full page — Read it for captions/flow; frameRenders are per-screen.'
    : 'No page overview resolved; frame renders only.',
});

packages/autolens-agent/src/tools/captureFigmaStoryboard.ts:119-127 (literal return shape)

constructed from type
{ "ok": true, "overviewImagePath": "figma-cache/EXAMPLE_FILE_KEY/renders/0_1.png",
  "frameRenders": { "146:308": ".../renders/146_308.png", "146:98": ".../renders/146_98.png" },
  "errors": [] }
plan_lens_from_storyboard → Lens BriefThe persisted understanding of the experience — what the lens does

What the agent does with this: shows summary, resolves openQuestions before Phase 2, persists this as the single source build_figma_frame/wire_screen_flow read.

export interface PlannerResult {
  success: boolean;
  brief?: LensBrief;
  briefPath?: string;
  summary?: string;
  openQuestions?: string[];
  openQuestionOptions?: string[][];
  error?: string;
  validationErrors?: string[];
  partial?: boolean; //true when the run ended without plan_finalize succeeding
  turns: number;
}
export interface LensBrief {
  schemaVersion: 1;
  lens: { name: string; genre: LensGenre; mode: 'selfie'|'world'|'common'; figmaFileKey: string; storyboardPage?: string };
  features: LensBriefFeature[];
  flow: { start: string; transitions: LensBriefTransition[] };
  scoring?: LensBriefScoring;
  interactions: LensBriefInteraction[];
  hints?: LensBriefHint[];
  openQuestions: string[]; //unknowns surfaced for the user — NEVER guessed silently
  openQuestionOptions?: string[][];
  resolvedDecisions?: LensBriefResolvedDecision[];
  entranceAnimations?: LensBriefEntranceAnimation[];
}

packages/autolens-agent/src/planner/runPlannerAgent.ts:21-35 (PlannerResult); src/lib/agent/lensBrief/types.ts:164-201 (LensBrief; features :76-98, transitions :102-111, scoring :113-129, variantSets :65-74)

constructed from type
{ "schemaVersion":1, "lens":{"name":"Visa Fan Type","genre":"personality-quiz","mode":"selfie",
    "figmaFileKey":"EXAMPLE_FILE_KEY"},
  "features":[
    {"name":"Question1","controller":"Question1Controller","kind":"family","layoutFrame":"146:98",
     "questionFlow":{"model":"single-screen","textSwapOnly":true}},
    {"name":"FanReveal","controller":"FanRevealController","kind":"family",
     "frames":["146:308","146:360","146:409","146:462","146:513"],
     "variantSets":[{"slot":"fan-type-display","variants":["fan_type_1","…","fan_type_5"],
       "drivenBy":"scoreBucket","default":"fan_type_1"}]}],
  "flow":{"start":"Question1","transitions":[{"from":"Question1","to":"Question2","trigger":"answerCommitted","transitionStyle":"fade"}]},
  "scoring":{"model":"bit-index","outcomes":5,"surpriseChance":0.1},
  "interactions":[{"feature":"Question1","kind":"tilt-to-select","inputModel":"both","feelTunables":{"tiltThreshold":0.35}}],
  "openQuestions":["Should the fan type persist to the camera-roll caption?"] }

Populated with real Visa frame ids + its known 5-fan-type mechanic (types.ts:58-63) — no real lens-brief.json exists on disk anywhere in the tree.

BuildManifestThe per-scene build plan — how to construct the scene

What the agent does with this: hands this + textureMap to build_from_manifest, which compiles it into ONE VirtualScene apply (never hand-writes anchors/masks/payload).

export interface BuildManifest {
  schemaVersion: typeof MANIFEST_SCHEMA_VERSION;
  lensMode: 'selfie' | 'world' | 'common';
  template: 'thearlab' | 'none';
  assets: BuildManifestAsset[];
  layers: BuildManifestLayer[];
  groups?: BuildManifestGroup[];
  scripts?: BuildManifestScript[];
  wiring?: BuildManifestWiring[];
}
export interface BuildManifestAsset {
  id: string; type: AssetType; source: AssetSource; name: string;
  figmaNodeId?: string; prompt?: string; path?: string; preset?: string;
}
export interface BuildManifestLayer {
  id: string; name: string; type: LayerType; anchor: AnchorRect;
  frame: FrameSlot; cameraLayer: CameraSlot; groupId?: string; renderOrder?: number;
  //…persistent/reuse/variantSlot/drivenBy/hint/animation fields trimmed
  assetRef?: string; stretchMode?: number; tintColor?: string; opacity?: number;
  flipX?: boolean; flipY?: boolean; expectedAspect?: number;
  text?: string; fontSize?: number; fontFamily?: string; textColor?: string; textAlignment?: TextAlignment;
}

src/lib/agent/lensBrief/manifest.ts:170-179 (BuildManifest); :41-55 (BuildManifestAsset); :58-129 (BuildManifestLayer)

constructed from type
{ "schemaVersion":1, "lensMode":"selfie", "template":"thearlab",
  "assets":[{"id":"q1_card","type":"texture","source":"figma","name":"Q1 Question Card","figmaNodeId":"146:120"}],
  "layers":[
    {"id":"question_card","name":"Q1 Question Card","type":"image","assetRef":"q1_card","frame":"safe",
     "cameraLayer":"main","anchor":{"left":-0.8,"right":0.8,"bottom":0.2,"top":0.7},"stretchMode":2,"expectedAspect":1.4},
    {"id":"catchphrase","name":"Catchphrase","type":"text","text":"You, my friend, are a","fontFamily":"Inter",
     "fontSize":20,"textColor":"#ffffff","frame":"safe","cameraLayer":"main",
     "anchor":{"left":-0.76,"right":0.76,"bottom":0.1,"top":0.18}}] }
materialize_assetsEvery texture/font installed, returned as a textureMap

What the agent does with this: passes textureMap/fontCache to build_from_manifest and resolvedVariantSlots to wire_screen_flow.

return reply({
  textureMap, fontCache, errors, installed: Object.keys(textureMap).length, total: rawAssets.length,
  organized: moved.size, reused, resolvedVariantSlots, variantSetErrors,
});

packages/autolens-agent/src/tools/materializeAssets.ts:277-280 (literal return shape — wider than the old doc, also organized/resolvedVariantSlots/variantSetErrors)

real fixture the textureMap UUIDs
{ "textureMap":{"q1_card":"06505b2e-8561-4074-b01e-515d43bc7833","q1_tilt_hint":"d6f0b7fe-548f-42bd-bf82-c381a62e12bf"},
  "fontCache":{"Inter":"b1e0…-font-uuid"}, "errors":[], "installed":2,"total":2,"organized":2,"reused":0,
  "resolvedVariantSlots":[{"slot":"fan-type-display","drivenBy":"scoreBucket",
    "variantTextureUUIDs":["59eefac8-…","254a1c45-…"],"defaultIndex":0}], "variantSetErrors":[] }

textureMap UUIDs are real, read from .autolens/asset-dedup.json (e.g. Q1 Question Card → 06505b2e-8561-4074-b01e-515d43bc7833). The rest of the shape (fontCache/resolvedVariantSlots values) is illustrative.

build_from_manifestOne VirtualScene apply — idMapping is the id→UUID handoff

What the agent does with this: reads idMapping to target the right component UUID for follow-up writes, checks skippedAsExisting (dedup ran), forwards aspectChecks to verify_semantics.

return result({
  ok: errors.length === 0,
  applied: applied.applied ?? 0,
  errors,
  idMapping,
  anchorsSet,
  created: (instructions.create ?? []).length,
  modified: Object.keys(instructions.modify ?? {}).length,
  deleted: (instructions.delete ?? []).length,
  skippedAsExisting,
  aspectChecks,
  validationWarnings: v.warnings,
  //…flowBlind fields trimmed
});
//idMapping KEY forms: SceneObject "$temp:so_<layerId>" (applyInstructions.ts:101,183)
//                      component  "$temp:so_<layerId>:<Type>#0" (manifest.ts:692)

packages/autolens-agent/src/tools/buildFromManifest.ts:284-297 (literal return shape); src/lib/build/applyInstructions.ts:101,183; src/lib/agent/lensBrief/manifest.ts:692

constructed from type idMapping KEY forms are exact; UUIDs illustrative
{ "ok":true,"applied":6,"errors":[],
  "idMapping":{
    "$temp:so_question_card":"a3f01c2e-…","$temp:so_question_card:ScreenTransform#0":"b7c92d40-…",
    "$temp:so_question_card:Image#0":"c1d83e50-…","$temp:so_catchphrase":"d2e94f60-…",
    "$temp:so_catchphrase:ScreenTransform#0":"e3fa5071-…"},
  "anchorsSet":2,"created":2,"modified":0,"deleted":0,"skippedAsExisting":["Logo"],
  "aspectChecks":[{"sceneObjectUUID":"a3f01c2e-…","expectedAspect":1.4}],"validationWarnings":[] }
build_figma_frame + facts.jsonThe Figma-frame keystone builder, and the append-only fact log it writes

What the agent does with this: applies suggestedAnimations via one apply_element_animation, chains nextSteps verifiers; a resumed run reads facts.json instead of recomputing.

//derivedFacts.ts — facts.json shape
export interface DerivedFact { key: string; value: unknown; at: string; }
interface FactsFile { schemaVersion: 1; facts: DerivedFact[]; }

//buildFigmaFrame.ts — literal return shape
return reply({
  ok: errors.length === 0, results, errors, summary, log,
  factsFile: FACTS_RELATIVE_PATH,
  factsNote: `Resolved node ids + constants were appended to ${FACTS_RELATIVE_PATH}…`,
  nextSteps: ['verify_geometry', 'verify_layer', 'verify_render_order'],
});

packages/autolens-agent/src/tools/derivedFacts.ts:9-18 (facts.json shape); buildFigmaFrame.ts:283-290 (return), :261-267 (facts keys written); FrameBuildResult/BuiltElement in figmaFrameBuilder.ts

constructed from type keys/values exact; UUIDs illustrative
{ "schemaVersion":1, "facts":[
  {"key":"frame:Question 1","value":{"nodeId":"146:98","width":334,"height":732,"aspectK":0.8112},"at":"…"},
  {"key":"container:Question 1","value":{"sceneObjectUUID":"f1a0…","name":"Question 1","parentRegion":"safe"},"at":"…"},
  {"key":"feature-container:Question1","value":{"name":"Question 1","sceneObjectUUID":"f1a0…"},"at":"…"},
  {"key":"element:Question 1:Q1 Question Card","value":{"figmaNodeId":"146:120",
    "sceneObjectUUID":"a3f01c2e-…","textureUUID":"06505b2e-8561-4074-b01e-515d43bc7833"},"at":"…"}]}

aspectK 0.8112 and the frame dims are real, asserted in src/lib/figma/scanFigmaFrame.test.ts:136. The UUIDs and at timestamps are illustrative.

wire_screen_flow → resolvedViaThe per-binding provenance stamp — how each @input actually bound

What the agent does with this: reads resolvedVia to see exactly HOW each @input bound (subtree vs scene-wide global vs ambiguous/missing) before verify_lens_states.

export interface WiredFeatureReport {
  feature: string; controller: string;
  scriptAssetUUID: string | null; scriptComponentUUID: string | null;
  boundInputs: string[]; unboundInputs: string[]; todos: string[];
  //container → 'arg' | 'facts:feature' | 'facts:frame' | 'brief:frame' | 'feature-name';
  //each element @input name → 'subtree' | 'global' | 'ambiguous' | 'missing'
  resolvedVia: Record<string, string>;
}
export interface WireFlowReport {
  success: boolean; features: WiredFeatureReport[];
  hub: { updatedInPlace: boolean; scriptPath: string | null; refsWired: string[]; refsPending: string[]; todos: string[] };
  recompile: { ran: boolean; note?: string };
  verify: { ran: boolean; logErrors: string[] };
  defects: string[]; nextSteps: string[];
}

src/lib/build/screenFlow/wire.ts:75-87 (WiredFeatureReport), :89-103 (WireFlowReport); resolvedVia values documented :83-85

constructed from type
{ "success":true, "features":[
  {"feature":"Question1","controller":"Question1Controller","scriptComponentUUID":"7b2f…",
   "boundInputs":["questionCard","answerLeft","answerRight","tiltHint"],"unboundInputs":[],"todos":[],
   "resolvedVia":{"container":"facts:feature","questionCard":"subtree","answerLeft":"subtree","tiltHint":"global"}}],
  "hub":{"updatedInPlace":true,"refsWired":["Question1Controller"]},"recompile":{"ran":true},"defects":[] }
verify_semantics / verify_geometry / verify_behaviorThe three gates that check meaning, sizing, and that a tap actually landed

What the agent does with this: types are exact; example values below are illustrative, not from a real run.

verify_semantics

interface SemanticDefect { severity: 'critical' | 'warning'; check: string; detail: string; }
//return: {ok, defects: SemanticDefect[]}
//real check names: inputTypeCheck | inputNamesNonEmpty | layerMask | aspectMismatch | textureLiveness | icHitArea

packages/autolens-agent/src/tools/verifySemantics.ts:17-21

constructed from type
{ "ok":false,"defects":[{"severity":"critical","check":"layerMask",
  "detail":"\"tilt-hint\" has layers=1048576, expected 2 — present in the scene, invisible on the camera that should show it."}] }

verify_geometry

const pass = matched > 0 && matched >= requested && residuals.length === 0 && groupOffsetResults.length === 0;
//return: {checked, matched, requested, layerSource, corrected[], residuals[], unmatched[], groupOffsetDefects[], pass}
//pass is always false on matched:0 — a soft "0 matched" note used to look like a pass; it can't anymore

packages/autolens-agent/src/tools/verifyGeometry.ts:345-351 (return shape), :338 (pass invariant)

constructed from type
{ "checked":4,"matched":4,"requested":4,"layerSource":"facts",
  "corrected":[{"name":"Q1 Question Card","builtWidthPct":52.0,"figmaWidthPct":63.0,"deltaWidthPct":11.0}],
  "residuals":[],"unmatched":[],"groupOffsetDefects":[],"pass":false }

verify_behavior

interface InteractionResult { name: string; passed: boolean; observedState: string | null; detail: string; }
//return: {ok, results: InteractionResult[]}

packages/autolens-agent/src/tools/verifyBehavior.ts:45-49

constructed from type
{ "ok":true,"results":[{"name":"tilt left to pick answer","passed":true,
  "observedState":"Question2","detail":"state advanced to expected 'Question2' after gesture"}] }

05 · Hard Rules

The 18 Hard Rules

HR = Hard Rule — each is a lesson compiled from a specific failed build; violating one is the cause of a "build came out broken."

HR1 Plan once: produce the BuildManifest before any scene work.

Prevents: improvising the next MCP call (the 300-call anti-pattern).

lens-experience-builder.md:100

HR2 All scene composition via ≤2 VirtualScene applies.

Prevents: per-layer MCP loops.

lens-experience-builder.md:104

HR3 Never hand-write the layer/group apply payload — call build_from_manifest.

Prevents: getting anchors/tint/flip/layer-masks wrong by hand.

lens-experience-builder.md:108

HR4 Hand-built @input wiring targets the COMPONENT UUID from idMapping.

Prevents: the silent no-op of wiring the SceneObject UUID.

lens-experience-builder.md:112

HR5 Self-audit before declaring done.

Prevents: "tool returned ok" being mistaken for "scene is correct."

lens-experience-builder.md:116

HR6 The TheARLab template is the mandatory base; resolve the frame UUID from roleUUIDs, never by name.

Prevents: content parented under the wrong camera/mode (a name exists under common and selfie and world).

lens-experience-builder.md:120

HR7 Budget turns so the verify phases are never amputated by the run cap.

Prevents: dying mid-verify with an unverified build (worse than a verified one with defects).

lens-experience-builder.md:124

HR8 Additive by default: never delete pre-existing objects; self-audit is read-only.

Prevents: deleting the user's own work (a self-audit once deleted a text element the user had just placed).

lens-experience-builder.md:128

HR9 Sizing is verified deterministically (verify_geometry), never by eye.

Prevents: shipped proportion errors vision judges miss (a 52%-vs-76% card).

lens-experience-builder.md:139

HR10 Build scene-by-scene, in the mandated phase order; delegate scene-builder for 3+ scenes.

Prevents: one-giant-manifest builds and paying Opus rates for a mechanical loop.

lens-experience-builder.md:143

HR11 A failed deploy_template is TERMINAL: stop, diagnose, fix, re-run.

Prevents: building on a partial scaffold (empty parent UUIDs).

lens-experience-builder.md:165

HR12 projectDir is the session's project, never a path lifted from a stale artifact.

Prevents: silently writing to the wrong project.

lens-experience-builder.md:176

HR13 The final report claims only what a readback confirmed.

Prevents: false "done" reports (assumed state ≠ verified state).

lens-experience-builder.md:185

HR14 No source archaeology: tools are contracts, skills are the docs.

Prevents: burning turns grepping AutoLens's own source (132 of 405 turns on one run).

lens-experience-builder.md:189

HR15 Every Agent-tool spawn is synchronous (run_in_background: false); never SendMessage/ScheduleWakeup.

Prevents: the "Stream closed" teardown that kills the rest of the run.

lens-experience-builder.md:200

HR16 Step-gated skill consultation: load the domain skill AT the gate, not after guessing wrong.

Prevents: repeated schema/API errors (13 GraphQL errors in one run, answered by an unopened skill).

lens-experience-builder.md:208

HR17 Every scene with transitions is graded against the motion-quality rubric before done.

Prevents: the four motion defects (re-entrance on advance, slide-not-pulse feedback, visible full-bleed edges, >1.2s entrance).

lens-experience-builder.md:224

HR18 The mandated verify suite is keyed to the feature set — gate selection is not discretionary.

Prevents: shipping with zero visual-fidelity checks or an unverified final mutation.

lens-experience-builder.md:243


06 · Skills & Sub-agents

Skills & sub-agents

The orchestrator composes AutoLens's own maintained skills with a read-only, allowlisted subset of CLAD's vendored skills — never the whole ~58-skill CLAD set.

skillAllowlist.ts — OWN_SKILLS lines 24-34, BORROW_SKILLS lines 46-55

Loaded own skills (9) AutoLens

"The skills we wrote" — the loaded set, from OWN_SKILLS (skillAllowlist.ts:24-34). Each row: what it teaches (with one real rule) + where in the pipeline it loads.

figma-import LOADED AutoLens

Teaches: Figma→LS screen-space conversion + the deterministic build chain; anchor math, Safe/Full routing, stretchMode by role. Real rule: bleeds beyond ±1 are correct and must NOT be clamped; z-order is compiler-owned (leave renderOrder unset).

Used: Phase 0 storyboard understanding + every per-frame Figma build.

plugins/autolens-builder/skills/figma-import/SKILL.md:47, lens-experience-builder.md:812

verify LOADED AutoLens

Teaches: build verification: silent-failure self-checks, the LS 5.22 capture path, the pre-done checklist. Real rule: RunAndCollectLogsTool ALWAYS resets the lens, so capture first / logs last, never interleaved; report only what a readback confirmed this session.

Used: the OWN half of the HR16 preview gate + HR17 motion grading + pre-completion.

plugins/autolens-builder/skills/verify/SKILL.md:119,287

mcp-tools LOADED AutoLens

Teaches: MCP conventions for LS 5.22: legacy→modern shim, VirtualScene property-type limits, the valueType table, no-deploy-scripts. Real rule: scene-graphql composite fields need a subselection — a GraphQL validation error returns HTTP 200 with {errors} and NO data, so ?? [] silently blinds a gate.

Used: effectively always-on tool-surface reference, before any MCP build.

plugins/autolens-builder/skills/mcp-tools/SKILL.md:114

ts-inputs LOADED AutoLens

Teaches: declaring @input params in LS 5.x TS. Real rule: the decorator + class extends BaseScriptComponent model is MANDATORY — //@input comment form is JS-only; in a .ts file it's NOT parsed → inputNames:{} → every wire silently no-ops.

Used: before authoring any .ts script — Step 2e stub filling, hub/controller authoring.

plugins/autolens-builder/skills/ts-inputs/SKILL.md:14

animation-architecture LOADED AutoLens

Teaches: the two-tier model (ONE LensController hub + Scene Controllers via global.CustomTween). Real rule: this.api = {} throws (api is reserved on BaseScriptComponent) — expose enable()/disable()/reset(); choice buttons enter with popIn, never dropIn (HR17).

Used: before any animation work — apply_element_animation + wire_screen_flow.

plugins/autolens-builder/skills/animation-architecture/SKILL.md:33

thearlab LOADED AutoLens

Teaches: deploy/use the TheARLab v3 base (5-camera rig, 9 phases) via deploy_template; role-UUID reuse; bundled hint wiring. Real rule: hint playback (autoplay/fps) is NOT agent-controllable — no author-time API exists, "the correct number of attempts is zero"; a failed deploy is TERMINAL.

Used: the deploy-base phase + whenever wiring hints.

plugins/autolens-builder/skills/thearlab/SKILL.md:161

quiz LOADED AutoLens

Teaches: quiz/question-flow lens: one persistent question screen (swap copy, don't rebuild), dual tap+tilt through one idempotent commit, bit-index scoring. Real rule: selectAnswer is idempotent — first commit (tap OR tilt) wins, second is a no-op.

Used: "the only genre skill loadable today," when building a sequential-question interaction.

plugins/autolens-builder/skills/quiz/SKILL.md:44

assets LOADED AutoLens

Teaches: asset management: two-level folders, dedup hygiene, lookup disambiguation, bundled-key imports. Real rule: there is NO automatic texture dedup — name-only dedup reused the wrong-colored box (2026-06-11), so textures import as "<name> <nodeId>".

Used: before installing packages / managing textures — materialize_assets, dedup, sweeps.

plugins/autolens-builder/skills/assets/SKILL.md:103

lens-field-notes LOADED AutoLens

Teaches: the master hard-rules field notes: component-UUID targeting, layer masks, the 7-step build sequence, item 18's gate table. Real rule: SetLensStudioProperty target = component UUID, silent no-op on SO UUID.

Used: "load before ANY LS MCP work"; item 18 is the gate table HR16 mirrors.

plugins/autolens-builder/skills/lens-field-notes/SKILL.md:37

Excluded own skills (6 — on disk, not loaded) AutoLens

Present in plugins/autolens-builder/skills/ but omitted from OWN_SKILLS — the SDK's skills filter is include-only, so exclusion is by omission, not deletion.

materials EXCLUDED AutoLens

Exclusion reason: LS-general duplicate → BORROW ls-clad:materials.

Teaches: material/render-order patterns. Real rule: never assign mainMaterial after CreateLensStudioComponent('Image') (silent no-op → invisible) — use CreateComponentFromPresetTool.

skillAllowlist.ts:17, plugins/autolens-builder/skills/materials/SKILL.md:16

scene-construction EXCLUDED AutoLens

Exclusion reason: LS-general duplicate → BORROW ls-clad:scene-construction.

Teaches: scene build/modify orchestration. Real rule: an MCP-created camera inherits near=1 which clips all screen content — must be near=-1 (A/B-proven live 2026-07-05).

plugins/autolens-builder/skills/scene-construction/SKILL.md:51

scripting EXCLUDED AutoLens

Exclusion reason: LS-general duplicate → BORROW ls-clad:editor-api/lens-api + own ts-inputs.

Teaches: runtime scripting conventions. Real rule: never re-InstallLensStudioPackage a script — it breaks every @input binding; write+save+RecompileTypeScriptTool is the only path that preserves bindings.

plugins/autolens-builder/skills/scripting/SKILL.md:243

randomizer EXCLUDED AutoLens

Exclusion reason: dead-template, pending template-library.

Teaches: a randomizer-template build. Rides the .js + InstallLensStudioPackage pipeline that's dead-by-API on LS 5.22.

skillAllowlist.ts:18 — orchestrator says do NOT load

photo-booth EXCLUDED AutoLens

Exclusion reason: dead-template.

Teaches: a photo-booth-template build. Real rule: array @inputs (flashMats/rts) must be written as the FULL array in one call — .N element writes fail silently.

plugins/autolens-builder/skills/photo-booth/SKILL.md:33

self-maintenance EXCLUDED AutoLens

Exclusion reason: maintenance/cron skill, dropped 2026-07-12 — wrong surface for the build agent; stays on disk, user-invocable.

Teaches: the learning-hub eval/approve loop. Real rule: the eval is the fixed point, and it NEVER merges — the PR is the human review gate.

skillAllowlist.ts:19-21

Notes: loaded=9 vs on-disk=15; the excluded 6 are dropped by omission, not deletion. The orchestrator's unqualified /scene-construction and /materials references resolve to the CLAD borrow, not the excluded own copies (same domain, different maintained source). lens-field-notes (loaded) still points at /self-maintenance (excluded) — fine for a human run, dead for the build agent. CLAD itself is ~58 skills; AutoLens borrows 8.

Borrowed ls-clad skills (8) CLAD

materials

Material creation/config/assignment; Editor/GraphQL-vs-Runtime-API distinction.

Loads: when creating materials / debugging rendering.
CLAD
vfx-graph

Custom .graphVfx particle graphs.

Loads: when building particle effects.
CLAD
lens-api

LS TypeScript runtime scripting (StudioLib): lifecycle, decorators, touch.

Loads: when writing runtime TS / handling tap / debugging null-refs.
CLAD
editor-api

The ExecuteEditorCode contract + Editor API paradigms.

Loads: before writing Editor API TS or grepping editor.d.ts.
CLAD
scene-construction

Orchestrator for scene build/modify; the preset-first workflow; indexes domain skills.

Loads: when constructing/modifying a scene.
CLAD
lens-debug

Triage router for LS runtime bugs (symptom → specialist).

Loads: given a symptom/error to diagnose.
CLAD
lens-log-analysis

LS log format, severity prefixes, reading RunAndCollectLogsTool.

Loads: when analyzing logs / debugging compile failures.
CLAD
snapchat-project-init

Snapchat in-camera Lens setup: camera tracking mode, preview via PreviewPanelTool.

Loads: when setting up/sanity-checking a Lens before preview.
CLAD

skillAllowlist.ts:47-54 (one line per name)

Footguns

15 skill dirs exist on disk but only 9 load — the allowlist excludes materials/scene-construction/scripting (LS-general duplicates, replaced by the CLAD-borrow versions above), photo-booth/randomizer (dead-template), and self-maintenance (dropped 2026-07-12). So materials/scene-construction load from plugins/clad/, not the own dir. Separately, the borrowed vfx-graph skill's stated "required reading," shader-graph, is not allowlisted — a dangling reference. CLAD itself is much larger (~58 skills) than the 8 AutoLens borrows.

skillAllowlist.ts:16-23

Sub-agents (6) AutoLens

lens-experience-builder model: claude-opus-4-8 · effort: high

The Opus orchestrator itself. Runs the four-phase pipeline; delegates the loops below.

plugins/autolens-builder/agents/lens-experience-builder.md

scene-builder model: claude-sonnet-5 · effort: medium

Cheaper per-scene executor. Runs the mechanical build→verify→fix loop for ONE scene. Mandatory delegation for 3+-scene storyboards, spawned sequentially.

plugins/autolens-builder/agents/scene-builder.md

lens-script-author model: inherit · effort: low

Fills wire_screen_flow's //TODO(agent) domain-trigger stubs + small leaf scripts; does not freehand whole controllers.

plugins/autolens-builder/agents/lens-script-author.md

lens-verifier model: inherit · effort: low

Read-only post-build structural verifier + the Step-2e stub-fidelity review gate.

plugins/autolens-builder/agents/lens-verifier.md

editor-api-specialist model: inherit · effort: low

Context shield for heavy ExecuteEditorCode work; landing spot for a flagged scene-builder escalation.

plugins/autolens-builder/agents/editor-api-specialist.md

figma-importer model: inherit · effort: low

Deep Figma analysis across many frames; assembles a BuildManifest fragment.

plugins/autolens-builder/agents/figma-importer.md


07 · Tools

Tools — 25 in-process + 21 Lens Studio MCP

The 25 in-process tools are AutoLens's own deterministic code the model can't skip; the 21 MCP tools are Lens Studio's own live editor surface — an AutoLens tool orchestrates many MCP calls into one verified operation (see Provenance).

25 in-process tools AutoLens

Plan (4)

plan_lens_from_storyboardscan Figma → validated Lens BriefAutoLens
scan_figmapure structured FigmaScanAutoLens
capture_figma_storyboardrender frames to PNG + surface captions/flowAutoLens
apply_element_animationrecord entrance-animation hints into the Brief; no scene writeAutoLens

Asset ingestion (1)

materialize_assetsinstall assets + fonts, return textureMapAutoLens

Build (4)

deploy_templatedeploy TheARLab scaffold → roleUUIDs / template-uuids.jsonAutoLens
build_from_manifestcompile a BuildManifest → ONE VirtualScene apply, owns all payload detailsAutoLens
build_figma_frameFigma-frame keystone: scan+build in one call, Safe/Full routing, dedupAutoLens
ensure_rigidempotently create/locate tracking-rig objectsAutoLens

Wire (2)

wire_screen_flowPhase-2 flow wiring: generate + bind controllers + recompile + verifyAutoLens
wire_script_inputsset named @input values on one ScriptComponentAutoLens

Verify (12)

verify_lens_statesdrive states, capture, vision-diffAutoLens
verify_semanticsLLM-free meaning checkAutoLens
verify_behaviorinject gesture → read stateNowAutoLens
verify_motionvision-grade transition feelAutoLens
verify_geometrydeterministic sizing + auto-correctAutoLens
verify_render_orderdeterministic z-orderAutoLens
verify_layerdeterministic camera-layer maskAutoLens
verify_scene_visualper-scene pixel diff vs FigmaAutoLens
capture_previewone side-effect-free snapshotAutoLens
capture_preview_sequencetimestamped frame seriesAutoLens
set_preview_sourceswitch preview input mediaAutoLens
dispatch_verify_fixesbatch mechanical fixes, escalate the restAutoLens

Learn / housekeeping (2)

record_learningfile a durable lessonAutoLens
sweep_unused_assetsexplicit orphan cleanup, dry-run by defaultAutoLens

packages/autolens-agent/src/pluginRunner.ts:207-213

21 Lens Studio MCP tools Lens Studio

External, over HTTP; namespace mcp__lens-studio__*; declared in the orchestrator frontmatter. Every tool below is Lens Studio's own — badged per row.

Scene/asset authoring (5)

VirtualSceneLens Studio
scene-graphqlLens Studio
asset-graphqlLens Studio
ExecuteEditorCodeLens Studio
GetBoundingBoxLens Studio

Scripting (4)

RecompileTypeScriptToolLens Studio
InstallLensStudioPackageLens Studio
ListInstalledPackagesToolLens Studio
ListAllPanelsLens Studio

Asset generation/search (7)

GenerateTextureLens Studio
FontSelectorLens Studio
ConvertSvgToTextureLens Studio
GenerateFaceMaskTextureLens Studio
GenerateLensIconLens Studio
SearchLensStudioAssetLibraryLens Studio
QueryLensStudioKnowledgeBaseLens Studio

Preview/runtime (5)

RunAndCollectLogsToolLens Studio
CaptureRuntimeViewToolLens Studio
CapturePanelScreenshotToolLens Studio
InjectPreviewGestureLens Studio
QueryRuntimeSceneToolLens Studio

lens-experience-builder.md:17-37

Plus, off the declared list

PreviewPanelTool — the side-effect-free screenshot path (~5.9fps, ~170ms/call, no side effects), preferred over CapturePanelScreenshotTool which clicks the panel and taps the lens mid-verification.

plugins/autolens-builder/skills/verify/SKILL.md:127


08 · Glossary

Glossary (11 terms)

Plain-English definitions. Use your browser's Ctrl-F / Cmd-F — everything here is always expanded, no custom search box.

codegen

The pure string-builder that turns a confirmed Lens Brief into house-style TypeScript feature-controller scripts + the regenerated LensController hub (no MCP calls, just source).

Matters because: it OWNS the motion law — the single place deciding how every element enters a screen (which helper, in what order, with what timing) and enforcing the owner's playtest "LAWs" (elements primed hidden; completion-chained order; static art never animates; full-bleed bitmaps never scale; whole entrance chain compresses to ≤1.2s). The "motion law" is this entrance-choreography grammar, not physics.

src/lib/build/screenFlow/codegen.ts:1,96-101,335-357,420-497

wire_screen_flow / wiring / resolvedVia

After controller scripts compile, wiring binds each script's @input fields to the actual scene (container @input → container SceneObject; element @input → element SceneObject UUID; content-text @input → Text component UUID) via setProperty. wire_screen_flow does the whole flow in one call; wire_script_inputs is the single-script version. resolvedVia is the per-binding provenance stamp recording HOW each input resolved (arg/facts:feature/facts:frame/brief:frame/feature-name; per element subtree/global/ambiguous/missing).

Matters because: an unbound @input = a controller referencing nothing = a silently dead screen. The log shows exactly which path landed each binding and which were left unbound rather than guessed.

src/lib/build/screenFlow/wire.ts:1,83-87,385-446

facts.json

.autolens/facts.json — an append-only log of things a build already figured out (resolved Figma node ids, computed constants, per-frame/element scene UUIDs) so a later stage reads them instead of recomputing blind after the process that held them died. build_figma_frame writes frame:*, container:*, feature-container:*, element:*.

Matters because: the builder names containers after the Figma FRAME, not the feature — without the container→feature map, wire_screen_flow guesses the feature name and misses every container (the 1316 wiring cascade).

packages/autolens-agent/src/tools/derivedFacts.ts:1, buildFigmaFrame.ts:261

stateNow

A public read-only @input string on the generated hub mirroring the current flow state, rewritten on every setState.

Matters because: the real field (currentState) is a private TS field LS never exposes at runtime — a machine verifier can't read it; verify_behavior reads stateNow back to assert the flow advanced. Without it the behavior gate has nothing to read.

codegen.ts:1216,1278, verifyBehavior.ts:27-33

template-uuids.json handshake

The single deploy→build handoff: deploy_template writes {TheARLab role → LS asset UUID} to .autolens/template-uuids.json; build_figma_frame/build_from_manifest read it back. Written atomically (temp + rename) so a crash can't leave a truncated file a downstream JSON.parse silently swallows into "no UUIDs."

Matters because: this was the 5-of-7-run "No template UUIDs available" failure.

packages/autolens-agent/src/tools/templateUUIDsStore.ts:1,27-44

BuildManifest vs Lens Brief

Two artifacts at two altitudes. Lens Brief (lens-brief.json, from the storyboard planner) = the understanding of the experience (features, flow, transitions, scoring): WHAT the lens does. BuildManifest (Phase 0, per scene) = the build plan (layers, anchors, assets, camera layers, render orders): HOW to construct that scene.

Matters because: codegen consumes the Brief; build_from_manifest consumes the manifest — mixing up which artifact answers which question is a common confusion.

src/lib/agent/lensBrief/manifest.ts:1-4, types.ts:16-102

VirtualScene apply ("one apply")

LS 5.22's declarative batch write: hand it every object/component/material as one payload, get back an idMapping of your ids → real UUIDs. "One apply" = build an entire scene in a single call instead of ~50-300 imperative calls (now dead-by-API/HTTP-501 on 5.22 anyway).

Matters because: the LLM never hand-writes anchors/tint/flip/masks — the compiler emits them deterministically.

buildFromManifest.ts:1-6, deployCore.ts:1-7, figmaFrameBuilder.ts:4-9

ExecuteEditorCode (EEC) + the component-UUID trap

EEC runs arbitrary JS against the LS editor API (bulk reads, scene walks, atomic multi-step mutations GraphQL can't express). The trap: layout/style props (anchor, stretchMode, text, material, an @input) must target the component UUID, NOT the SceneObject UUID — targeting the SO UUID returns success but is a silent no-op.

Matters because: "MCP success ≠ done" — this is the single most common way a write silently no-ops.

editor-api-specialist.md:19-33, scene-construction/SKILL.md:147

ortho camera / layer mask

A 2D Orthographic Camera draws screen-space UI; each visual carries a layer mask and a camera only renders layers its mask includes. Captured UI → Main Ortho 1048576; preview-only hints → Live Only Ortho 2.

Matters because: a UI object left on its default layer=1 is drawn by NO camera and renders nothing — no log, no error — so the deploy is silently invisible.

scene-construction/SKILL.md:36,40-43, figmaFrameBuilder.ts:41-42

harvest / learnings / tombstone

A learning = a durable reusable lesson under .autolens/learned/{pending,approved,rejected}/. harvest deterministically mines a run's NDJSON into draft candidates; record_learning files one live; both land in pending/; a human approves/rejects; approved lessons inject into every future prompt (cap 60). A tombstone = a rejected/<id>.json whose presence means "never re-propose this id."

Matters because: retire-superseded.mts mass-moved 51 approved → 18 — stale lessons mislead if never retired.

packages/autolens-agent/src/learning.ts:1-4,20-41, recordLearning.ts:1-10, learning/harvestRun.ts:1-2, electron/learning/learnedStore.ts:14, scripts/learning/retire-superseded.mts:1-3

the verify_* gates

Matters because: "the tool returned ok" is not "done" — each gate below checks a different way a build can look right and not be right.

GateChecksModeFile
verify_semanticsobject exists AND means what the wiring assumes (rig type, ts-input, mask, aspect, dead texture)deterministic (no LLM)verifySemantics.ts:1-5
verify_geometryeach layer's live anchor box matches the aspect-normalized suggestedAnchor; auto-fixesdeterministicverifyGeometry.ts:1-6
verify_scene_visualper-scene, pre-behavior: isolate scene, capture, vision-diff vs Figmavision (Sonnet gate)verifySceneVisual.ts:1-5
verify_behaviorinject one gesture per interaction, settle, read stateNow back, assert advancedeterministic probeverifyBehavior.ts:1-5
verify_motiongrade a frame time-series for feel (continuity/easing/z-order/flicker/timing)vision (stronger model)verifyMotion.ts:1-9
verify_layerwhich camera draws each visual (Main Ortho vs Live Ortho); fixes invisible casedeterministicverifyLayer.ts:1-4
verify_render_orderlive renderOrder matches the role-resolved z-order; fixes driftdeterministicverifyRenderOrder.ts:1-4
verify_lens_statesdrive each state's debugState, capture, grep logs, vision-diff vs FigmahybridverifyLensStatesTool.ts:1-4

09 · Failure Map

Why tests go nowhere — honest framing

Builds run long and stall because tools break silently, not because the model hallucinates. A 2026-07-12 hallucination audit of a live run refuted the "hallucinating too much" theory — 1 confident-wrong-acted-on claim in ~87 checked assertions, zero fabricated identifiers, zero shipped defects from hallucination. Cross-run forensics (7 archives) found build phases are near-constant (~60-90 turns); all the variance lives in the post-build tail (302 tail-turns worst vs 4 best; ~4× cost spread).

The failure classes below each have a fix implemented in the reliability wave (commit ef5d693) and hardened by a follow-up Opus review fleet (9b3b13d — 21 agents, 4 more sub-defects fixed, 3 deferred with evidence), reviewed GO — but not yet live-validated (test-18 is the validator). Read these as diagnosed + fix landed + review-hardened, awaiting live validation — never "solved."

Symptom seen in tests Root cause Fix that landed Status
"No template UUIDs available" — deploy looked fine but the build had no parents deploy→build handshake drift/corruption; triplicated read logic + non-atomic write; failed 5/7 runs atomic canonical template-uuids.json handoff (see Glossary: template-uuids.json handshake) ef5d693 + 9b3b13d, awaiting test-18
~148 turns of hand-wiring recovery; "Input elSelfieFeed was not provided" wire_screen_flow failed 100% of calls (5/5 across 3 runs): builder names containers by Figma frame, wirer searched by feature name; no scene-root path for shared elements facts.json container→feature map + UUID-first resolution + whole-graph fallback + resolvedVia provenance (see Glossary); HR-driven ef5d693 + 9b3b13d, awaiting test-18
~100-turn "tap marathon" — every tap read observedState:"" the hub kept currentState private → verify_behavior structurally blind → false-fail on every transition hub exposes readable stateNow mirror; verify_behavior probes it first (see Glossary: stateNow) ef5d693 + 9b3b13d, awaiting test-18
Downstream taps swallowed Intro's armTap on a scene-root full-screen object survived disable() (no IC teardown in codegen) disable() disarms what armTap armed ef5d693 + 9b3b13d, awaiting test-18
2 of 7 runs shipped geometry-unverified; gates noted matched:0 instead of failing soft gates: verify_geometry returned "NOTHING was verified" as a note; verify_semantics false-RED on the {UUID,name} reference form gates hard-fail (matched:0pass:false); auto-load facts.json; parse {UUID,name} ef5d693 + 9b3b13d, awaiting test-18
21/37 EEC calls in one run were pure API probes; three incompatible scene-walk idioms no canonical editor-JS snippet → the agent re-derives the editor API by introspection one pinned ExecuteEditorCode idiom in mcp-tools (HR14/HR16) ef5d693 + 9b3b13d, awaiting test-18
~4.2k tokens/build of injected learnings, ~65% dead or misleading learnings store went NET-NEGATIVE (51 approved = 16 keep / 20 superseded by code / 7 merge / 7 demote / 1 contradicts); no retirement trigger mechanical retirement → 18 approved / 43 tombstoned (see Glossary: harvest / learnings / tombstone) ef5d693, done (store state)