{"slug":"comic-asset-review-loop","title":"comic-asset-review-loop","summary":"Phase-1 (S5) UPSTREAM ref-asset gate — the bounded cross-model adversarial loop that LOCKS one reusable identity-locked asset (character sheet / location plate / prop cutout / text-panel / logo-free symbol) BEFORE it can be composited into any panel. This is NOT the panel_gate (c","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-06T17:21:24.769102Z","repo":{"url":"https://github.com/wanshuiyin/ARIS-Movie-Director","stars":64,"forks":4,"license":"MIT","updatedAt":"2026-09-28T07:48:51Z"},"bodyHtml":"<hr>\n<h2>name: comic-asset-review-loop\ndescription: \"Phase-1 (S5) UPSTREAM ref-asset gate — the bounded cross-model adversarial loop that LOCKS one reusable identity-locked asset (character sheet / location plate / prop cutout / text-panel / logo-free symbol) BEFORE it can be composited into any panel. This is NOT the panel_gate (composed-output) or the assembly_gate (cross-panel) — it gates the refs that FEED both, so identity drift is caught at the source, not downstream. Two layers: (1) a MANDATORY static single-source collision check (check_asset_collisions.py — 'one visual dialect, never two', enforced by tool) + zero-text/literal build-asserts; (2) a per-round 3-reviewer panel (Claude narrative ‖ Gemini visual ‖ Codex synth) that blind-scores 5 dims and LOCKS only at 准×3 unanimity (prior_lock_count&gt;=3 approvals in the SAME round) — else regenerate (route to comic-asset-ref-generator) or, at MAX_REVIEW_ROUNDS, escalate the asset REQUIREMENT back to outline/storyboard. Hard IP veto. Use when you say 'lock this character ref', 'asset gate', 'ref review', 'is this sheet reusable', '锁角色 ref', before any metered panel bake.\"\nargument-hint: [asset_id | --batch-from-outline | --lite]\nallowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, mcp__codex__codex, mcp__codex__codex-reply, mcp__gemini__analyzeFile, mcp__gemini-cli__ask-gemini, mcp__oracle__consult</h2>\n<h1>comic-asset-review-loop — the UPSTREAM ref gate (Phase 1 · S5)</h1>\n<p>The <strong>ref gate the comic side was missing.</strong> <code>comic-director</code>'s <code>panel_gate</code> reviews <em>composed output</em>; the\n<code>assembly_gate</code> reviews <em>cross-panel consistency</em>. Neither gates the <strong>identity refs that feed both</strong> — so\nwithout this skill, identity drift is only caught <em>downstream</em>, after credits are spent on a panel bake. This\nskill is the <strong>Layer-2 gate on a SINGLE reusable reference asset</strong> (the character sheets / location plates /\nprop cutouts the <code>ART_BIBLE.md</code> points every bake at), run <strong>before</strong> any panel composition.</p>\n<p>A reusable asset is a one-way door: once a hundred panels condition on <code>duo_canonical_ref_v001.png</code>, you cannot\nquietly swap it. So the lock must be <strong>earned cross-model</strong>, never self-certified by the generator. This skill\nlocks an asset <strong>only at 准×3</strong> — Claude <em>and</em> Gemini <em>and</em> Codex each independently pass the <strong>same</strong> review\nround — or it sends the asset back to be regenerated, or (at the round ceiling) escalates the asset\n<em>requirement</em> itself back to the outline/storyboard layer. The executor (and the generation model family)\n<strong>never</strong> acquit their own ref.</p>\n<p><strong>Pipeline position (the Phase-1 asset DAG — the documented contract):</strong> this lock runs AFTER the\nhuman-approved outline (OUTLINE_DRAFT_VALID only requires every referenced asset to be DECLARED with a\ncomplete, generatable request — never pre-locked assets) and the <strong>provisional</strong> storyboard's\n<code>consolidated_asset_requests</code>, and BEFORE OUTLINE_FINAL_LOCK (cheap re-check that the locked assets still\nmatch the approved outline) → storyboard FINAL asset-resolution validation → blueprint authoring, where the\nhard locked-asset barrier sits. (The old single-stage contract — outline gate demanding locked assets —\ndeadlocked and is retired.)</p>\n<pre><code>  asset (review_status:\"pending\")  ◀── comic-asset-ref-generator (S4: generates, NEVER self-locks)\n        │\n        ▼\n   LAYER 1 — STATIC CI GATE (deterministic, zero credits)\n        check_asset_collisions.py  (one canonical owner per filename — \"one visual dialect, never two\")\n        + build asserts: zero-text contract · literal contract · raster-ref 6-field completeness\n        │  pass\n        ▼\n   LAYER 2 — VISUAL LOOP   (round R = 1 … MAX_REVIEW_ROUNDS=3)\n        ① Claude narrative  ‖  ② Gemini visual (primary: identity/iso)  ‖  ③ Codex synth (policy/semantic)\n        each reads the REAL asset file · scores 5 dims (0–5 ints) · flags failure_modes from the fixed vocab\n        │\n        ▼ Codex synthesises: MIN-fuse per dim (max for inverted) · threshold · per-reviewer unanimity\n   准×3 ? ── no, a family HARD-FAILS a floor ─▶ comic-asset-ref-generator --regenerate (bump version) ─▶ ① R+1\n        │ no, third family just ABSENT (e.g. Gemini down) ─▶ re-VOTE (re-collect the missing reviewer; NO re-bake)\n        │ no (safety_ip&lt;3)  ─▶ HARD abandon_shot (IP veto, no retries)\n        │ no (R == MAX_REVIEW_ROUNDS) ─▶ ESCALATE the asset REQUIREMENT to outline/storyboard (abandon_shot)\n        │ yes\n        ▼\n   LOCK  — asset.status=locked + payload.review_status=locked  (ONE-WAY DOOR, immutable for the rest of Phase 1)\n        + score_progression audit table (ASSET_REVIEW.md) + decision/review/failure_mode wiki nodes + edges\n</code></pre>\n<h2>Constants</h2>\n<ul>\n<li><strong>REVIEWERS</strong> = Claude (this agent, narrative) ‖ <code>mcp__gemini__analyzeFile</code> (<code>model: \"auto-gemini-3\"</code>, NEVER\nraw <code>gemini-3.1-pro</code> — 429 silent-downgrade to 2.5) ‖ <code>mcp__codex__codex</code> r1 / <code>mcp__codex__codex-reply</code> r2+\n(<code>model_reasoning_effort: \"xhigh\"</code>, save the <code>threadId</code>; pin <strong>no</strong> model — Codex reviewer calls follow the\nlocal codex config, currently <code>gpt-5.6-sol</code>, exactly like the shipped Codex CLI reviewers in <code>run_comic.py</code>,\nwhich pass no <code>-m</code>; only the upstream BAKE payload pins <code>gpt-5.5</code> + <code>xhigh</code>, the <code>run_comic.get_bake_plan()</code>\ncompat default — a config-driven override there is planned, not yet implemented).</li>\n<li><strong>准×3 UNANIMITY</strong> — the lock predicate. A Codex-synth <code>approve</code> is <strong>NOT</strong> enough: <code>cc_approves AND gemini_approves AND codex_approves</code> must ALL be true <strong>in the same round</strong> (each reviewer's <em>own</em> dim scores\nmust independently clear threshold). Codex-approve-without-unanimity is <strong>DOWNGRADED to <code>revise</code></strong> (the \"准×3\nbite\"). <code>prior_lock_count</code> = the count of approvals accumulated this round; <strong>lock iff prior_lock_count &gt;= 3</strong>.</li>\n<li><strong>MAX_REVIEW_ROUNDS</strong> = 3 (hard ceiling). Even if Codex keeps saying <code>regenerate</code>, the loop STOPS at 3 and\nroutes back to outline/storyboard — the <em>requirement</em> changes; the gate does not grind an image forever.</li>\n<li><strong>CROSS-MODEL ACQUITTAL</strong> — Codex is the <strong>synth/policy</strong> reviewer and (in the broader pipeline) the\ngeneration family for raster refs. A Codex <code>approve</code> can <em>diagnose/veto</em> but is <strong>never the sole acquitter</strong>;\nthe lock needs Gemini approve + Claude approve too (acceptance-gate.md: the loop DRIVES, it can't ACQUIT).</li>\n<li><strong>SAFETY_IP SINGLE-VOTE VETO</strong> — <code>safety_ip &lt; 3</code> from <strong>ANY</strong> reviewer = immediate HARD <code>abandon_shot</code>, no\nretries (generalises <code>comic-director</code>'s anatomy single-vote veto to IP/likeness).</li>\n<li><strong><code>--lite</code> is NON-ACQUITTING</strong> — when Gemini is unavailable it may set only a PROVISIONAL\n<code>status: under_review</code> (tag <code>lite_provisional: true</code>), <strong>NEVER a root <code>status: locked</code>.</strong> The one-way LOCK\nrequires the THIRD family (Gemini) to lock-pass in a later round: 准×3 is CC AND Gemini AND Codex same-round\nunanimity (canon), and the only sanctioned relaxation is the gate's \"a model is UNAVAILABLE → threshold-only\nPROVISIONAL, exit 2\" — never a Gemini-absent lock. Use <code>--lite</code> only to keep progress while Gemini is down;\nnever the default; it can never write the locked one-way door.</li>\n<li><strong>OUTPUT</strong> = <code>examples/&lt;project&gt;/story/ASSET_REVIEW.md</code> (the human score-progression audit) + wiki nodes/edges\nunder <code>wiki/nodes/</code> + per-reviewer trace under <code>.aris/traces/comic-asset-review-loop/</code>.</li>\n</ul>\n<h2>Input contract / what this skill owns</h2>\n<p>The contract boundary is the <strong><code>asset</code> wiki node</strong> authored by <a href=\"../comic-asset-ref-generator/SKILL.md\"><code>comic-asset-ref-generator</code></a> (S4) at\n<code>review_status: \"pending\"</code> — generated, never self-locked. This skill is <strong>pure verify + lock</strong>:</p>\n<ul>\n<li>It <strong>never edits the asset's pixels or geometry</strong> — on <code>regenerate</code>/<code>revise</code> it routes a ≤140-char\n<code>repair_instruction</code> back to S4, which bumps the version and emits a new file.</li>\n<li>It <strong>never invents</strong> scores or a verdict on behalf of an absent reviewer — a dim no reviewer scored is\n<code>not_scored</code> and <strong>EXCLUDED</strong> from the threshold, never substituted with 0 (the v1.0 bug; see Hard do/don't).</li>\n<li>It writes back <strong>only</strong> <code>status: \"locked\"</code> + <code>payload.review_status: \"locked\"</code> on a 准×3 approve, plus the\nreview/decision/failure_mode trace. <code>locked</code> is the one-way door — immutable for the rest of Phase 1.</li>\n</ul>\n<p><strong>Read</strong> the <code>asset</code> node payload (per <code>schemas/node_schema.json</code>, <code>node_type: \"asset\"</code>,\n<code>payload.required = [asset_kind, name, visual_description, identity_lock, ref_requirements, review_status, version]</code>):</p>\n<ul>\n<li><code>asset_kind</code> ∈ {character, scene, prop, text_panel, logo_free_symbol} — <strong>routes the per-reviewer query</strong>\n(see §\"Asset-kind specialisation\").</li>\n<li><code>identity_lock.{must_preserve[], must_avoid[]}</code> — the per-asset rubric (e.g. from <code>ART_BIBLE.md</code> §1:\nexecutor = blue hoodie <code>#1D4684</code> + brown hair + <strong>no beard</strong>; reviewer = green hoodie <code>#30582D</code> + near-black\nhair + <strong>beard</strong>; \"颜色/胡子/体型轮廓不可变\").</li>\n<li><code>ref_requirements.{aspect_ratio, background (white|transparent), pose, isolated_reference}</code>.</li>\n<li><code>output_ref.{file_path, data_url, sha256, width, height, mime}</code> for a raster ref (all 6 fields, else schema\nviolation) <strong>OR</strong> <code>{generator_script, owner_script, file_sha256}</code> for a deterministic SVG/JSON source.</li>\n</ul>\n<h2>The EXACT gate (ported from <code>asset-review-loop</code>)</h2>\n<h3>Layer 1 — static CI gate (deterministic, MANDATORY, zero credits)</h3>\n<p>Run <strong>before</strong> any visual reviewer call (and re-run before lock). Fail-closed; correctness costs no credits.</p>\n<ul>\n<li><strong>single-source collision check</strong> — <code>check_asset_collisions.py</code> static-scans every <code>gen_*.py</code> with the regex\n<code>\\bw\\(\\s*[\"']([^\"']+\\.(?:svg|json|png))[\"']</code> and asserts <strong>each output filename has EXACTLY ONE generator\nowner</strong>. <code>len(writers) &gt; 1</code> → <code>exit 1</code>. This is the tool that enforces <strong>\"one visual dialect, never two\"</strong>:\ntwo generators writing the same path with different content is a silent, run-order-dependent corruption\n(whoever runs last wins). It is deliberately <strong>NOT</strong> a \"skip if file exists\" guard — that would <em>hide</em> the\norder bug instead of surfacing it. Verdict: <code>exit 0</code> (clean) is a <strong>precondition</strong> for entering Layer 2;\n<code>exit 1</code> blocks the round and routes to S4 to rename/delete the duplicate. (Verified clean at 26 outputs in\nthe worked example.)</li>\n<li><strong>zero-text contract</strong> — for any asset that must contain no glyphs (e.g. the S22 constellation):\n<code>assert \"&lt;text\" not in svg</code> in the emitter. A zero-text asset that contains text fails the round.</li>\n<li><strong>literal contract</strong> — for a deterministic SVG/JSON source, its <code>_contract</code> field forbids re-layout-by-eye;\nthe JSON is the single coordinate truth (e.g. <code>wiki_starmap_nodes_v1.json</code> for S16b labeled + S22 wordless).</li>\n<li><strong>raster 6-field completeness</strong> — a raster <code>output_ref</code> has all of <code>file_path, data_url, sha256, width, height, mime</code>; <code>sha256</code> matches the file on disk (a partial write is a schema violation, not a lockable ref).</li>\n</ul>\n<h3>Layer 2 — the 5-dim visual rubric (0–5 integers each)</h3>\n<p>Each reviewer <strong>reads the real asset file</strong> (image bytes via <code>Read</code> / <code>analyzeFile</code>) and scores all five; the\n<strong>Primary</strong> reviewer's score is authoritative for that dim, the <strong>Secondary</strong> corroborates.</p>\n<ol>\n<li><strong>identity_lock_satisfied</strong> — preserves EVERY <code>must_preserve</code> trait, avoids EVERY <code>must_avoid</code>.\n<strong>Primary = Gemini (visual)</strong>, Secondary = Codex (semantic). character → face/age/hair/costume/beard;\nscene → layout + lighting-type + era.</li>\n<li><strong>ref_quality</strong> — sharp, well-framed, correctly exposed, artefact-free. <strong>Primary = Gemini</strong>,\nSecondary = Claude. (text_panel → glyph crispness weighted.)</li>\n<li><strong>bg_isolation</strong> — background is EXACTLY the required colour, isolated, no halo / shadow-spill / other\nsubjects. <strong>Primary = Gemini ONLY.</strong> For a <strong>scene</strong> this dim becomes <strong>scene_emptiness</strong> (no characters or\nprops sneaked into a location plate), same 0–5 scale.</li>\n<li><strong>reuse_readiness</strong> — compositable into multiple downstream conditions without per-shot rework.\n<strong>Primary = Claude (usage_fit)</strong>, Secondary = Gemini (composability).</li>\n<li><strong>safety_ip</strong> — no celebrity likeness / copyrighted character / brand logo. <strong>HIGHER = SAFER</strong> (5 = clean).\n<strong>Primary = Codex (policy)</strong>, Secondary = Gemini (recognition).</li>\n</ol>\n<h3>EXACT verdict thresholds (Codex synth) + the 准×3 bite</h3>\n<blockquote>\n<p><strong>These are THIS skill's pre-gate routing thresholds (the asset-gate step-3 routing this skill OWNS), and\nthey are written to MATCH <a href=\"../comic-cross-layer-gate/SKILL.md\"><code>comic-cross-layer-gate</code></a>'s <code>--gate asset</code>\nLOCK predicate verbatim.</strong> The gate's <code>LOCKED</code> condition is <code>identity_lock_satisfied &gt;= 4 AND ref_quality &gt;= 4 AND bg_isolation &gt;= 4 AND safety_ip &gt;= 4</code> <strong>AND</strong> the <code>准 ×3</code> same-round cross-model unanimity rule —\n<code>reuse_readiness</code> is <strong>ADVISORY</strong> at the gate and does <strong>not</strong> block the lock. The <code>approve</code> rule below uses\nexactly those four floors; <code>reuse_readiness</code> only feeds <code>revise</code>/<code>regenerate</code> routing, never blocks the lock.</p>\n</blockquote>\n<p>Codex <strong>MIN-fuses</strong> each dim across the reviewers who scored it (the gate's deterministic rule: <strong>min</strong> per dim,\n<strong>max</strong> for an inverted dim, single scorer → use it, NO scorer → <code>not_scored</code> <strong>EXCLUDE</strong> — <strong>never 0-substitute,\nnever average</strong>: averaging <code>{5,3}</code>→4 would slip a sub-floor ref past the <code>&gt;=4</code> lock; min=3 correctly fails). So\nthis inline routing can only be <strong>stricter</strong>, never weaker, than <code>--gate asset</code>. The legal asset verdict set is\n<code>{approve, regenerate, locked, abandon_shot}</code> (gate-enforced); the internal <code>revise</code> below is an alias that\nroutes a <code>regenerate</code>. Then:</p>\n<ul>\n<li><strong><code>approve</code></strong> ← <code>identity_lock_satisfied &gt;= 4</code> <strong>AND</strong> <code>ref_quality &gt;= 4</code> <strong>AND</strong> <code>bg_isolation &gt;= 4</code>\n<strong>AND</strong> <code>safety_ip &gt;= 4</code> (the four gate floors). <strong><code>asset_kind == \"character\"</code> override:</strong>\n<code>identity_lock_satisfied == 5</code> is REQUIRED (a raster character ref locks only at a <em>perfect</em> identity score —\n4 is not enough for a face hundreds of panels condition on); the other three floors stay <code>&gt;= 4</code>. (准×3\nsame-round unanimity still applies on top — see the LOCK note below.)</li>\n<li><strong><code>regenerate</code></strong> ← <code>identity_lock_satisfied &lt; 4</code> (or <code>&lt; 5</code> for a character) OR <code>bg_isolation &lt; 4</code> OR\n<code>ref_quality &lt; 4</code> OR <code>safety_ip ∈ [3, 4)</code> OR <code>reuse_readiness &lt; 3</code> (advisory: routes a regen hint, does not\nitself block a lock that already clears the four floors).</li>\n<li><strong><code>revise</code></strong> ← mid-band, a single targeted edit likely fixes it (softer hint, still routes a <code>regenerate</code>\nwith a focused <code>repair_instruction</code>).</li>\n<li><strong><code>abandon_shot</code></strong> ← <code>safety_ip &lt; 3</code> (HARD STOP, IP) <strong>OR</strong> <code>round &gt;= MAX_REVIEW_ROUNDS</code> without convergence.</li>\n</ul>\n<blockquote>\n<p><strong>Deterministic-SVG / JSON asset branch</strong> (a parametric builder, NOT a raster ref → the visual 5-dim rubric\ndoes not apply). A deterministic-SVG source <strong>locks iff ALL FOUR hold</strong> (Layer-1 asserts + a 3-dim 0–5 read):\n<code>single_source_owner == pass</code> (Layer-1 <code>check_asset_collisions.py</code> exit 0 — exactly one generator owns the\nfilename) <strong>AND</strong> <code>render_preview_legible &gt;= 4</code> (the SVG/JSON rasterizes to a non-empty, on-spec preview PNG —\na RAW render pre-check, not a vote) <strong>AND</strong> <code>literal_contract &gt;= 4</code> (the <code>_contract</code> field is honored: literals\nare ascii-tokenizable, the JSON is the single coordinate truth, no eyeball re-layout) <strong>AND</strong>\n<code>style_family_consistency &gt;= 4</code> (one visual dialect — palette/stroke/grid match the <code>ART_BIBLE.md</code> family).\nPlus the <strong>zero-text / non-gated-text asserts</strong>: a wordless source contains no <code>&lt;text&gt;</code> (<code>assert \"&lt;text\" not in svg</code>); a labeled source's glyphs are exactly the contract's literals. Same 准×3 same-round unanimity gates\nthe SVG lock as the raster lock. (Gemini PRIMARY on <code>render_preview_legible</code> + <code>style_family_consistency</code>;\nCodex PRIMARY on <code>literal_contract</code>; <code>single_source_owner</code> is the Layer-1 tool, not a reviewer vote.)</p>\n</blockquote>\n<blockquote>\n<p><strong>LOCK requires 准×3 UNANIMITY, not Codex-synth-approve.</strong> Even on a Codex <code>approve</code>, the lock fires <strong>iff</strong>\n<code>cc_approves AND gemini_approves AND codex_approves</code> in the <strong>same</strong> round (each reviewer's own 5 scores\nindependently clear the per-reviewer thresholds — including the <code>identity_lock_satisfied == 5</code> floor for a\ncharacter ref / the four-dim SVG floors for a deterministic source). A Codex <code>approve</code> with any reviewer below\nthreshold is <strong>DOWNGRADED to <code>revise</code></strong>. (<code>--lite</code> is <strong>non-acquitting</strong>: with Gemini absent it may only write\nprovisional <code>status: under_review</code> + <code>lite_provisional: true</code>, and can NEVER satisfy the LOCK predicate or reach P6.) This same-round\nunanimity rule is exactly <a href=\"../comic-cross-layer-gate/SKILL.md\"><code>comic-cross-layer-gate</code></a>'s <code>准 ×3</code> for\n<code>--gate asset</code> (this skill is the OWNER of that convention) — NOT \"three consecutive rounds\".</p>\n</blockquote>\n<h3>Veto rules (hard, non-negotiable)</h3>\n<ul>\n<li><strong>safety_ip &lt; 3 from ANY reviewer → immediate <code>abandon_shot</code></strong> (single-vote IP veto, no retries).</li>\n<li><strong>Layer-1 fail (collision / missing literal / partial raster / zero-text breach) → block the round</strong>, route\nto S4, do not enter Layer 2.</li>\n<li><strong>round &gt;= MAX_REVIEW_ROUNDS without 准×3 → ESCALATE the asset <em>requirement</em></strong> to\n<a href=\"../comic-outline-creator/SKILL.md\"><code>comic-outline-creator</code></a> / <a href=\"../comic-storyboard-creator/SKILL.md\"><code>comic-storyboard-creator</code></a>\n(the spec/outline must change; stop re-baking the image). Mutual recursion is bounded by this ceiling.</li>\n</ul>\n<h3>Controlled failure-mode vocabulary (the FIXED set reviewers draw tags from)</h3>\n<p><code>face_drift, age_drift, costume_drift, pose_off, multi_subject_leak, halo_artifact, shadow_spill, non_white_bg, jpeg_artifact, glyph_break, unintended_text, watermark_present, contact_sheet, celeb_likeness, copyrighted_character, brand_logo_present, collage_grid, low_resolution, melted_anatomy</code>. Every reject mints a\n<code>failure_mode</code> wiki node <code>fail:&lt;asset-slug&gt;_&lt;tag&gt;</code> tagged from this set (the <code>&lt;tag&gt;</code> must match <code>[a-z0-9_]+</code> —\nstrip any stray punctuation before assembling the node_id; maps to <code>comic-director</code>'s anatomy/identity vetoes) and routes\nits tag into the S4 <code>repair_instruction</code>. The node MUST carry the schema-required shape (copy the <code>fail:*</code>\nskeleton in \"Wiki node skeletons\" below <strong>verbatim</strong>): root <code>status: \"active\"</code> + payload <code>{layer, affected_shot_ids, active, repair_pattern}</code>. Because this is an <strong>upstream asset gate with no shots yet</strong>,\n<code>affected_shot_ids</code> is <code>[]</code> (empty) — never invented. NB the shipped example failure nodes are FILES named\n<code>fail_*.json</code> (the colon lives only inside the <code>node_id</code>, e.g. <code>fail:s09_a01</code>) and are ALL downstream\nshot-level nodes (<code>layer:\"panel_visual\"</code>, non-empty <code>affected_shot_ids</code> — <code>fail_s09_a01.json</code> carries\n<code>[\"S09\"]</code>); <strong>no asset_ref-layer example exists in the repo</strong>, so do NOT copy the example nodes for this gate —\nthe skeleton below is the authoritative shape.</p>\n<h2>Wiki node skeletons (the EXACT shape each minted node MUST carry — validated by <code>cli/validate_wiki.py</code>)</h2>\n<p>Every node this skill writes obeys <code>schemas/node_schema.json</code>: the <strong>root</strong> carries <code>node_id, node_type, title, status, created_at, payload</code> (all six required), the <code>node_id</code> matches <code>^(…|review|decision|fail):[a-z0-9_-]+$</code>,\nand the <code>payload</code> carries every field in <code>validate_wiki.py</code> <code>PAYLOAD_REQUIRED</code> for that type. The canonical root\n<code>status</code> for each runtime type (schema $comment; the real example nodes carry exactly these): <code>review → \"complete\"</code>, <code>decision → \"final\"</code>, <code>failure_mode → \"active\"</code>. Copy these skeletons verbatim:</p>\n<pre><code>// review:*  — one per reviewer per round (P1 cc, P2 gemini, P3 codex). node_id: review:&lt;asset-slug&gt;_&lt;reviewer&gt;_round{R}\n// PAYLOAD_REQUIRED[\"review\"] = [target_node_id, reviewer, gate_kind]  (review_scores carries the 5 dims)\n{ \"node_id\": \"review:duo_canonical_ref_v001_cc_round1\", \"node_type\": \"review\", \"status\": \"complete\",\n  \"title\": \"CC narrative asset review duo_canonical_ref v1 r1\", \"created_at\": \"&lt;ISO-8601&gt;\",\n  \"payload\": { \"target_node_id\": \"asset:&lt;asset-slug&gt;\", \"reviewer\": \"cc\", \"gate_kind\": \"asset\",\n               \"review_scores\": { \"identity_lock_satisfied\": 5, \"ref_quality\": 4, \"bg_isolation\": 4,\n                                  \"reuse_readiness\": 4, \"safety_ip\": 5 },\n               \"failure_modes_flagged\": [], \"timed_out\": false } }\n\n// decision:*  — one per round after the synth+准×3 (P4). node_id: decision:asset_&lt;asset-slug&gt;_round{R}\n// PAYLOAD_REQUIRED[\"decision\"] = [target_node_id, verdict, gate_kind]\n{ \"node_id\": \"decision:asset_duo_canonical_ref_v001_round1\", \"node_type\": \"decision\", \"status\": \"final\",\n  \"title\": \"asset_gate duo_canonical_ref v1 r1 -&gt; locked\", \"created_at\": \"&lt;ISO-8601&gt;\",\n  \"payload\": { \"target_node_id\": \"asset:&lt;asset-slug&gt;\", \"gate_kind\": \"asset\", \"verdict\": \"approve\",\n               \"reasoning\": \"LOCK 准×3 — identity=5 ref=4 bg=4 safety=5 reuse=4; cc&amp;gemini&amp;codex approve r1\",\n               \"repair_instruction\": \"\", \"prior_lock_count\": 3 } }\n\n// fail:*  — only on a reject/abandon_shot (P0 Layer-1 fail, P2 parse failure-mode, P5 reject). node_id: fail:&lt;asset-slug&gt;_&lt;tag&gt;\n// PAYLOAD_REQUIRED[\"failure_mode\"] = [layer, affected_shot_ids, active]   (upstream gate → affected_shot_ids: [])\n{ \"node_id\": \"fail:duo_canonical_ref_v001_costume_drift\", \"node_type\": \"failure_mode\", \"status\": \"active\",\n  \"title\": \"asset_gate duo_canonical_ref costume_drift\", \"created_at\": \"&lt;ISO-8601&gt;\",\n  \"payload\": { \"layer\": \"asset_ref\", \"affected_shot_ids\": [], \"active\": true,\n               \"repair_pattern\": \"executor 蓝衣棕发无须 (must_preserve); 当前绿衣漂移 — 重生为 #1D4684 hoodie\" } }\n</code></pre>\n<p>After any lock (P6), run <code>python3 cli/validate_wiki.py examples/&lt;project&gt;</code> — it MUST PASS (root status enum,\nnode_id form, payload invariants). A node missing root <code>status</code> or <code>target_node_id</code> / <code>layer</code> /\n<code>affected_shot_ids</code> / <code>active</code> is the exact failure the validator catches.</p>\n<h2>The two fail-closed engine contracts (where they bite here)</h2>\n<p>This is an <strong>upstream</strong> gate, so it enforces the <em>preconditions</em> the downstream engine fail-closes on. <strong>Do NOT\nconflate the three SVG fields</strong> (per <code>schemas/node_schema.json</code> + the engine): the wiki\n<code>blueprint.payload.content_svg</code> (top-level on the blueprint payload), the <code>panel_spec.payload.content_blueprint</code>\n(the panel_spec's own field — there is NO <code>content_svg</code> on a panel_spec), and the <strong>comic.json</strong> panel's\n<strong>runtime</strong> <code>condition.content_svg</code> (the field the engine reads at <code>spiral_engine.js</code> <code>.condition.content_svg</code>).</p>\n<ol>\n<li><strong>Every panel needs a renderable blueprint SVG.</strong> A locked asset is only useful if a panel can condition on\nit — so when locking a deterministic SVG/JSON source, verify it rasterizes to a non-empty PNG (a <code>blueprint</code>\nnode would carry it as <code>blueprint.payload.content_svg</code>), because at bake time the engine <strong>refuses</strong> a\ncomic.json panel whose <code>condition.content_svg</code> is <code>null</code>/absent. A ref that cannot become a content-SVG\ncondition is not \"reuse-ready\" — score <code>reuse_readiness</code> (and, for an SVG source, <code>render_preview_legible</code>)\naccordingly. <strong>Never <code>condition.content_svg: null</code>.</strong></li>\n<li><strong>A baked figure-panel needs <code>condition.expected_literals</code>.</strong> If the asset is a <strong>text_panel</strong> / figure\nsource whose downstream panels bake numbers (e.g. a wandb curve, a verdict stamp, a DDL widget), its lock\nMUST verify the literal contract holds (the literals are ascii-tokenizable and the JSON <code>_contract</code> forbids\neyeball re-layout) — because <code>comic-director</code>'s gate <strong>refuses to run</strong> a baked figure-panel whose comic.json\n<code>condition.expected_literals</code> is empty. Locking a figure source that cannot supply gateable literals would\nstrand the panel; a scene panel with no audited numbers takes <code>text_mode: \"html\"</code> instead.</li>\n</ol>\n<h2>Numbered procedure (per round R, start R = 1, loop until 准×3 or R = 3)</h2>\n<p><strong>P0 — Resolve + Layer-1 gate.</strong> Resolve the target <code>asset</code> node (single <code>asset_id</code>, or fan out one branch per\nasset under <code>--batch-from-outline</code>). Assert <code>review_status == \"pending\"</code>; <strong>abort if already <code>locked</code></strong> (one-way\ndoor). Route by <code>asset_kind</code> (visual raster kinds → the raster 5-dim branch; a deterministic SVG/JSON source →\nthe SVG 4-dim branch; an audio kind would take the audio branch — out of scope for the comic). <strong>Run Layer 1</strong>\n(<code>check_asset_collisions.py</code> + build-asserts + raster 6-field check); on any failure, write a <code>failure_mode</code>\nnode (root <code>status: \"active\"</code>, payload <code>{layer:\"asset_ref\", affected_shot_ids:[], active:true, repair_pattern: &lt;the Layer-1 tag-derived hint&gt;}</code> per the <code>fail:*</code> skeleton) + a <code>failure_of</code> edge <code>fail:&lt;…&gt; → asset:&lt;asset-slug&gt;</code>,\nroute to S4, and stop — do not spend a visual reviewer call.</p>\n<p><strong>P1 — Claude narrative review.</strong> <code>Read</code> the PNG (image bytes). Score all 5 dims (Claude <strong>PRIMARY</strong> on\n<code>reuse_readiness</code>; Codex is PRIMARY on <code>safety_ip</code> per the rubric), emit <code>failure_modes_flagged[]</code> (from the\nfixed vocab) + <code>narrative_notes</code>. Write a review-stage scratch JSON and a wiki <code>review</code> node\n<code>review:&lt;asset-slug&gt;_cc_round{R}</code> with <strong>root <code>status: \"complete\"</code></strong> and payload carrying the three\nPAYLOAD_REQUIRED fields <strong><code>target_node_id: \"asset:&lt;asset-slug&gt;\"</code>, <code>reviewer: \"cc\"</code>, <code>gate_kind: \"asset\"</code></strong> plus\n<code>review_scores</code> keyed by the 5 dims (see the <code>review:*</code> skeleton above). Append a <strong><code>reviews</code></strong> edge\n<code>review:&lt;…&gt; → asset:&lt;asset-slug&gt;</code> (review → target — the direction the gate walks; NEVER <code>reviewed_by</code>).\n<strong>Atomic write</strong> (<code>.tmp</code> → <code>mv</code>).</p>\n<p><strong>P2 — Gemini visual review</strong> (skip iff <code>--lite</code>). Call <code>mcp__gemini__analyzeFile</code> with <code>model: \"auto-gemini-3\"</code> (NEVER raw <code>gemini-3.1-pro</code>), the PNG path, and a <strong>strict-JSON-only</strong> template that asks for\nthe 5 dim scores (Gemini <strong>PRIMARY</strong> on <code>identity_lock_satisfied</code>, <code>ref_quality</code>, <code>bg_isolation</code>) +\n<code>failure_modes</code> drawn from the fixed vocab. <strong>Retry once</strong> on non-JSON, then log a <code>gemini_parse_failure</code> and\ntreat those scores as <code>null</code> (not 0). Optional focused follow-up if <code>identity_lock_satisfied &lt; 3</code>. Write the\n<code>review</code> node <code>review:&lt;asset-slug&gt;_gemini_round{R}</code> with <strong>root <code>status: \"complete\"</code></strong> and payload\n<strong><code>target_node_id: \"asset:&lt;asset-slug&gt;\"</code>, <code>reviewer: \"gemini\"</code>, <code>gate_kind: \"asset\"</code></strong> + <code>review_scores</code>; append\nthe <strong><code>reviews</code></strong> edge <code>review:&lt;…&gt; → asset:&lt;asset-slug&gt;</code> (review → target). On a parse failure mint a <code>fail:*</code>\nnode per the <code>fail:*</code> skeleton (root <code>status: \"active\"</code>, payload <code>{layer:\"asset_ref\", affected_shot_ids:[], active:true, repair_pattern:\"gemini_parse_failure — re-elicit strict JSON\"}</code>).</p>\n<p><strong>P3 — Codex synth gate.</strong> <code>mcp__codex__codex</code> (r1) / <code>mcp__codex__codex-reply</code> (r2+ with the saved\n<code>threadId</code>), effort <code>xhigh</code>, no model pin (follows the local codex config — currently <code>gpt-5.6-sol</code>). Pass\n<strong>ONLY</strong> numeric scores + <code>failure_modes</code> + <code>asset_metadata</code> + file paths</p>\n<ul>\n<li>(optional) numeric prior-round deltas — <strong>NEVER</strong> any reviewer <code>narrative_notes</code>/prose (reviewer-independence;\na prose leak ABORTS the call with a loud error). Codex adds its OWN <code>safety_ip</code> (PRIMARY) + semantic\n<code>identity_lock_satisfied</code> + <code>reuse_readiness</code> scores. <strong>First persist Codex's OWN reviewer node</strong> (mirroring P1\ncc / P2 gemini): <code>review:&lt;asset-slug&gt;_codex_round{R}</code> (root <code>status: \"complete\"</code>, payload <code>{target_node_id: \"asset:&lt;asset-slug&gt;\", reviewer: \"codex\", gate_kind: \"asset\", review_scores: {the 5 dims Codex scored}}</code>) + a\n<code>reviews</code> edge <code>review:&lt;…&gt;_codex_round{R} → asset:&lt;asset-slug&gt;</code> — so all THREE reviewer families exist as\nfirst-class collected nodes (准×3 needs Codex as one of the three the gate walks). THEN synthesise: <strong>MIN-fuse</strong>\neach dim across the reviewers who scored it (max for an inverted dim; single scorer → use it; no scorer →\n<code>not_scored</code>, EXCLUDE — never average / 0-substitute) → <code>verdict</code> + <code>per_reviewer_unanimity{cc,gemini,codex}</code> +\n<code>repair_instruction</code> (≤140 chars) + <code>rationale</code> (≤300 chars — the ONLY Codex prose allowed out).</li>\n</ul>\n<p><strong>P4 — Apply threshold + 准×3 → <code>decision</code> node.</strong> Apply the threshold, then the unanimity downgrade. Write a\n<code>decision</code> node <code>decision:asset_&lt;asset-slug&gt;_round{R}</code> with <strong>root <code>status: \"final\"</code></strong> and payload carrying the\nthree PAYLOAD_REQUIRED fields <strong><code>target_node_id: \"asset:&lt;asset-slug&gt;\"</code>, <code>verdict</code>, <code>gate_kind: \"asset\"</code></strong> plus\n<code>reasoning</code>, <code>repair_instruction</code>, <code>prior_lock_count</code> (see the <code>decision:*</code> skeleton above). <code>approve</code> +\n<code>prior_lock_count &gt;= 3</code> (准×3) → mark <strong>lock</strong>. <code>approve</code> without unanimity → downgrade to <code>revise</code>, which is\n<strong>persisted as <code>verdict: regenerate</code></strong> — the only legal <code>decision.payload.verdict</code> tokens are\n<code>{approve, regenerate, locked, abandon_shot}</code> (a verdict outside the gate's set is a hard error); <code>revise</code> is an\ninternal route label kept in <code>reasoning</code>/<code>repair_instruction</code>, never written as the verdict. A TERMINAL\nfail — <code>abandon_shot</code> (IP veto) OR the <code>R == MAX_REVIEW_ROUNDS</code> escalation (P5) — sets <strong>BOTH</strong> root\n<code>asset.status: \"rejected\"</code> AND <code>payload.review_status: \"rejected\"</code> (canon D2/D3: terminal-fail → root <code>status</code>\nrejected; mirror the lock path, which sets both). A non-terminal <code>revise</code>/<code>regenerate</code> keeps root\n<code>status: \"under_review\"</code> (provisional), never leaving an abandoned asset stuck at <code>pending</code>.</p>\n<blockquote>\n<p><strong>Gate handoff (this skill is the asset gate's review-producer).</strong> P1–P3 have now persisted the <code>review:*</code>\nscore-nodes + <code>reviews</code> edges that <a href=\"../comic-cross-layer-gate/SKILL.md\"><code>comic-cross-layer-gate</code></a> <code>--gate asset</code> collects — so the gate is <strong>never invoked cold</strong>. This skill runs the asset-gate synthesis inline (it\nOWNS 准×3); when the orchestrator defers acquittal to <code>comic-cross-layer-gate &lt;asset:&lt;asset-slug&gt;&gt; --gate asset</code> instead, it fuses the SAME <code>review:*</code> nodes via those <code>reviews</code> edges and re-derives the <code>&gt;=4</code>-floor +\n准×3 verdict. NOTE this skill applies an ADDITIONAL stricter pre-gate floor (character\n<code>identity_lock_satisfied == 5</code>; the deterministic-SVG 4-dim branch for an identity-4 ref), so its inline\nverdict can be <strong>stricter</strong> (never weaker) than the bare gate's <code>&gt;=4</code> floor — the deferred gate does not\nreproduce the <code>==5</code> character rejection byte-for-byte. The <code>decides</code> edge (P5) links this <code>decision</code> → the asset.</p>\n</blockquote>\n<p><strong>P5 — Edges + route.</strong> Write edges using ONLY <code>validate_wiki.py</code> <code>EDGE_TYPES</code>: a <strong><code>reviews</code></strong> edge\n<code>review:&lt;…&gt; → asset:&lt;asset-slug&gt;</code> per reviewer (review → target — the direction <code>--gate asset</code> walks; NEVER\n<code>reviewed_by</code>, the opposite direction, which the gate cannot follow), the <strong><code>decides</code></strong> edge\n<code>decision:asset_&lt;…&gt; → asset:&lt;asset-slug&gt;</code>, and on a reject/abandon_shot a <strong><code>failure_of</code></strong> edge\n<code>{src: \"fail:&lt;asset-slug&gt;_&lt;tag&gt;\", dst: \"asset:&lt;asset-slug&gt;\", type: \"failure_of\"}</code> (failure_mode → target,\nmatching the real <code>examples/comic_m3_audit/wiki/edges.jsonl</code>). A regen that supersedes the prior version writes a\n<strong><code>supersedes</code></strong> edge <code>asset:&lt;new&gt; → asset:&lt;old&gt;</code> (bare node_ids, no <code>@vN</code>). Then route:</p>\n<ul>\n<li><strong>third family merely ABSENT this round</strong> (a reviewer didn't vote — e.g. Gemini down, or <code>--lite</code>) and NO\nfamily hard-failed a floor → <strong>RE-VOTE</strong>: re-collect ONLY the missing reviewer (re-run that one family); do\n<strong>NOT</strong> <code>--regenerate</code> (re-baking wastes a credit when the image is fine and only the third vote is missing).</li>\n<li><code>regenerate</code>/<code>revise</code> (a family HARD-FAILED a floor) <strong>and R &lt; 3</strong> → call\n<a href=\"../comic-asset-ref-generator/SKILL.md\"><code>comic-asset-ref-generator</code></a> <code>--regenerate</code> with the\n<code>repair_instruction</code> (it bumps the version, emits a new PNG with a <code>supersedes</code> self-edge, re-fires at <strong>R+1</strong>).</li>\n<li><code>regenerate</code>/<code>revise</code> <strong>and the re-bake count reaches <code>MAX_ASSET_REGEN</code> = 4</strong> (the gate's re-bake cap,\n<a href=\"../comic-cross-layer-gate/SKILL.md\"><code>comic-cross-layer-gate</code></a> lines 70/184 — a SEPARATE ceiling from this\nskill's own <code>MAX_REVIEW_ROUNDS = 3</code> visual review-round limit; either hitting its ceiling escalates) → force\n<strong><code>abandon_shot</code></strong> → <strong>ESCALATE the requirement</strong> to outline/storyboard.</li>\n<li><code>abandon_shot</code> (IP) → exit immediately.</li>\n</ul>\n<p><strong>P6 — Lock</strong> (on <code>approve</code> + full 准×3 = CC AND Gemini AND Codex same-round unanimity). Set <code>status: \"locked\"</code></p>\n<ul>\n<li><code>payload.review_status: \"locked\"</code>; append tags <code>[asset_gate_locked, round_{R}_approve]</code>. (<strong><code>--lite</code> can NOT\nreach P6</strong> — with Gemini down it stays provisional <code>under_review</code>, never locked.) Validate against\n<code>schemas/node_schema.json</code> (<code>python3 cli/validate_wiki.py examples/&lt;project&gt;</code>); append a <code>log.md</code> line; write\nthe final <strong><code>ASSET_REVIEW.md</code> score_progression</strong> table (the human audit trail — <em>why</em> it locked at r2 vs r3\nvs abandoned). <strong>Lock = ONE-WAY DOOR</strong>, immutable for the rest of the pipeline.</li>\n</ul>\n<h2>Asset-kind specialisation (drives the per-reviewer query)</h2>\n<ul>\n<li><strong>character</strong> → identity_lock weights face/age/hair/costume/<strong>beard</strong> (the <code>ART_BIBLE.md</code> §1 iron law:\nexecutor never bearded, reviewer always bearded; colours immutable, L/R position free).</li>\n<li><strong>scene</strong> → <code>bg_isolation</code> becomes <strong>scene_emptiness</strong> (a location plate must carry no characters/props).</li>\n<li><strong>prop</strong> → standard 5 dims; <code>bg_isolation</code> weighted (clean cutout, no spill).</li>\n<li><strong>text_panel</strong> → glyph crispness weighted into <code>ref_quality</code> (maps to <code>comic-director</code>'s\n<code>baked_text_quality</code>); the literal contract (engine contract #2) is checked at lock.</li>\n<li><strong>logo_free_symbol</strong> → <strong>extra <code>safety_ip</code> weight</strong> (the symbol must not resemble a real brand mark).</li>\n</ul>\n<h2>The judge-is-broken guard (battle lesson — land it, don't just say it)</h2>\n<p><strong>IDENTICAL scores across review rounds mean the rubric/judge is broken — STOP regenerating and audit the\njudge.</strong> If round R and round R+1 (on a <em>regenerated</em> asset) return the <strong>same</strong> per-dim vector from the same\nreviewer, the artifact changed but the score did not → the <strong>rubric</strong> (not the asset) is wrong. Concretely:</p>\n<ul>\n<li><strong>Detect</strong>: after P4 on R+1, compare the per-reviewer dim vectors to round R's. If <code>cc</code>/<code>gemini</code>/<code>codex</code>\nreturn a byte-identical 5-vector on a <em>new</em> file (different <code>sha256</code>), <strong>flag <code>judge_stuck</code></strong>.</li>\n<li><strong>Halt the bake</strong>: do NOT route another <code>--regenerate</code>. Two identical rounds is the ceiling for <em>grinding</em>\neven before MAX_REVIEW_ROUNDS.</li>\n<li><strong>Audit the judge</strong> instead: re-read <code>identity_lock.{must_preserve,must_avoid}</code> and the <code>ART_BIBLE.md</code> carve-\nouts — is the gate flagging a <strong>by-design</strong> trait as drift? (The classic: the two-world warm/dark palette is\n<em>intentional</em> — <code>ART_BIBLE.md</code> §0.5 \"冷暖是 by-design 的,不是漂移\"; a naive gate scores it \"style drift\"\nevery round. The <code>motif</code>-vs-<code>env</code> disambiguation is the same lesson: the gate must know which instances are\ncontinuity-bearing.) Fix the rubric (sharpen the per-dim definition / add the carve-out to the reviewer\nquery), record it as a <code>decision</code> trace node, then resume. <strong>A stuck score is a judge bug, not an asset bug.</strong></li>\n</ul>\n<h2>Worked example</h2>\n<p>Ground every run in the real M3-audit comic asset set under <code>examples/comic_m3_audit/</code> (repo-relative). The exact patterns to copy:</p>\n<ul>\n<li><p><strong>Layer-1 collision gate</strong> — <code>gen/check_asset_collisions.py</code> (41 lines) is the single-source CI gate\nverbatim: the <code>WRITE = re.compile(r'\\bw\\(\\s*[\"']([^\"']+\\.(?:svg|json|png))[\"']')</code> scan over every <code>gen_*.py</code>,\nthe <code>len(writers) &gt; 1</code> collision rule, the <code>exit 0/1</code> contract, and the load-bearing comment that it is\n<strong>deliberately NOT</strong> a \"skip if the file already exists\" guard (\"that would HIDE the order bug instead of\nsurfacing it\"). Its message — <em>\"Each asset must have ONE canonical owner ('one visual dialect, never two')\"</em>\n— is the literal embodiment of this skill's single-source rule. <strong>Verified clean at 26 outputs (exit 0).</strong>\nRun this as Layer 1 of P0; do not enter Layer 2 until it returns 0.</p>\n</li>\n<li><p><strong>The identity-lock rubric</strong> — <code>ART_BIBLE.md</code> §1 (<code>角色身份锁</code>) is the literal <code>must_preserve</code>/<code>must_avoid</code>\nsource for an <code>identity_lock_satisfied</code> score: authority ref <code>duo_canonical_ref_v001.png</code>; per-character hex\ntable (executor blue hoodie <code>#1D4684</code> / brown hair <code>#925935</code> / <strong>NO beard</strong>; reviewer green hoodie <code>#30582D</code>\n/ near-black hair <code>#0F0D16</code> / <strong>beard</strong>); the 铁律 <em>\"executor 永远蓝+棕发+无须;reviewer 永远绿+黑发+有须 …\n颜色/胡子/体型轮廓不可变\"</em>. A Gemini visual reviewer cat's the PNG and scores it against <strong>this</strong> — not by\nvibes. §0.5 (the warm/dark two-world palette, <em>\"冷暖是 by-design 的,不是漂移\"</em>) is the canonical carve-out\nfor the judge-is-broken guard above. §7 FORBIDDEN (no smooth gradients on character surfaces, no voxel, no\nnon-canonical mascot colours, no off-identity beard) is the <code>must_avoid</code> list.</p>\n</li>\n<li><p><strong>The P0-blocker pattern</strong> — <code>story/STORYBOARD_DRAFT.md</code> flags <code>researcher_chibi_canonical_ref_v001.png</code> as\n<strong>missing</strong> (only the duo had a canonical ref). That is exactly the state this gate must catch <em>before</em> any\npanel bakes the researcher: a raster ref with no file is not lockable (Layer-1 raster 6-field check fails);\nit routes to S4 to generate the ref, and only then does Layer 2 run. <strong>Copy this:</strong> a panel that conditions\non a ref whose asset node is not <code>locked</code> is a pre-bake blocker, surfaced here, never discovered downstream.</p>\n</li>\n</ul>\n<p>Pattern to copy end-to-end: <strong>Layer 1 (deterministic, free) gates single-source + completeness → Layer 2\n(cross-model, metered only after L1 passes) gates the 5 dims against the <code>ART_BIBLE.md</code> rubric → lock at 准×3\nor route back to S4 (regenerate) / outline (escalate).</strong></p>\n<h2>Hard do / don't (earned lessons)</h2>\n<ul>\n<li><strong>DO</strong> run the static collision gate (<code>check_asset_collisions.py</code>) as Layer 1 of <strong>every</strong> run, before any\nreviewer call — \"one visual dialect, never two\" is enforced by the tool, not by prose.</li>\n<li><strong>DO</strong> lock <strong>only at 准×3</strong>: Claude AND Gemini AND Codex pass the SAME round. A Codex-synth <code>approve</code> alone\ndowngrades to <code>revise</code>. The generator family never self-acquits its own ref.</li>\n<li><strong>DO</strong> treat <code>safety_ip &lt; 3</code> from <strong>any one</strong> reviewer as an immediate hard <code>abandon_shot</code> — IP is a single-vote\nveto, no retries (the same shape as the anatomy single-vote veto).</li>\n<li><strong>DO</strong> stop at MAX_REVIEW_ROUNDS=3 and escalate the asset <strong>requirement</strong> to outline/storyboard — the spec\nchanges; the gate does not grind one image forever.</li>\n<li><strong>DON'T</strong> zero a missing dim. A dim no reviewer scored is <code>not_scored</code> and <strong>EXCLUDED</strong> from the threshold —\nsubstituting 0 was the v1.0 bug that mis-rejected good refs.</li>\n<li><strong>DON'T</strong> keep regenerating when rounds return <strong>identical</strong> scores on a <em>changed</em> file — that means the\n<strong>judge</strong> is broken (a by-design trait scored as drift). Audit the rubric, not the asset.</li>\n<li><strong>DON'T</strong> leak reviewer prose into the Codex synth call — pass numeric scores + failure_modes + paths only;\na prose leak ABORTS the gate (reviewer-independence).</li>\n<li><strong>DON'T</strong> edit the asset's pixels here. On <code>regenerate</code>/<code>revise</code>, route a ≤140-char <code>repair_instruction</code> to\n<code>comic-asset-ref-generator</code>; this skill only verifies and locks.</li>\n<li><strong>DON'T</strong> re-open a <code>locked</code> asset — the lock is a one-way door; a later need for a different ref is a NEW\nasset (new file, <code>supersedes</code> edge), not an edit of the locked one.</li>\n</ul>\n<h2>Scope</h2>\n<table>\n<thead>\n<tr>\n<th>Target</th>\n<th>Fit</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>reusable character sheet / location plate / prop cutout (identity-locked raster ref)</td>\n<td><strong>excellent</strong></td>\n</tr>\n<tr>\n<td>deterministic SVG/JSON asset source (one parametric builder per recurring motif)</td>\n<td><strong>excellent</strong></td>\n</tr>\n<tr>\n<td>text_panel / figure source whose downstream panels bake gateable literals</td>\n<td>good (checks the literal contract)</td>\n</tr>\n<tr>\n<td>logo-free symbol / mark (extra safety_ip weight)</td>\n<td>good</td>\n</tr>\n<tr>\n<td>a <em>composed</em> panel's faithfulness</td>\n<td>no → that's <code>comic-director</code>'s <code>panel_gate</code></td>\n</tr>\n<tr>\n<td>cross-panel continuity (motif drift across the film)</td>\n<td>no → that's <code>comic-continuity-audit</code> / the assembly_gate</td>\n</tr>\n</tbody>\n</table>\n<h2>Protocols (governance contracts this skill honors)</h2>\n<ul>\n<li><a href=\"../../protocols/reviewer-independence.md\"><code>reviewer-independence</code></a> — each reviewer reads the real asset file; the Codex synth gets <strong>numeric scores + failure_modes + paths only</strong>, never another reviewer's prose (a leak ABORTS).</li>\n<li><a href=\"../../protocols/acceptance-gate.md\"><code>acceptance-gate</code></a> — the loop DRIVES (regenerate, re-fire R+1) but can't ACQUIT: the LOCK is a Type-B verdict that needs 准×3 (Gemini + Codex + Claude in the same round); Codex-synth-approve alone is downgraded; same-family N-of-Claude is never a jury.</li>\n<li><a href=\"../../protocols/review-tracing.md\"><code>review-tracing</code></a> — every Codex/Gemini reviewer call's full prompt+response is saved under <code>.aris/traces/comic-asset-review-loop/&lt;date&gt;_run&lt;NN&gt;/</code>, so each lock/abandon_shot verdict is auditable; the <code>ASSET_REVIEW.md</code> score_progression is the human-readable trail.</li>\n<li><a href=\"../../protocols/reviewer-routing.md\"><code>reviewer-routing</code></a> — Codex reviewer calls pin <strong>no</strong> model (local codex config — currently <code>gpt-5.6-sol</code>) at effort <code>xhigh</code> (only the upstream bake payload pins <code>gpt-5.5</code> + <code>xhigh</code>, the <code>run_comic.get_bake_plan()</code> compat default; config-driven override planned); Gemini <code>auto-gemini-3</code> (never raw <code>gemini-3.1-pro</code>); <code>— reviewer: oracle-pro</code> routes the synth to <code>mcp__oracle__consult</code> (the Pro tier) on explicit request; never downgrade the tier.</li>\n</ul>\n","files":[{"path":"SKILL.md","sizeBytes":40910,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-06T17:21:44.231508Z","sha256":"77B13DB0CEF11D9D61F28D8D70B89E79F4D7775C7857FF98C91D2C0543A7B386","sizeBytes":15991},"review":null,"source":{"repositoryUrl":"https://github.com/wanshuiyin/ARIS-Movie-Director","path":"skills/comic-asset-review-loop","license":"MIT","commit":"2eb3d1660de5fbea538084aea5d92394505c663a","subtreeSha":"577DE237A9E10145F8FF08F6A886DDCA829404B0E11D909B5C3B063826546684","lastSyncedAt":"2026-09-30T15:23:40.11157Z"},"reviewedAt":"2026-09-06T17:22:02.927978Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/wanshuiyin/ARIS-Movie-Director/tree/main/skills/comic-asset-review-loop"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wanshuiyin-aris-movie-director@llmmart"},{"target":"git","command":"git clone https://github.com/wanshuiyin/ARIS-Movie-Director.git"}]}