comic-asset-review-loop — the UPSTREAM ref gate (Phase 1 · S5)

SkillAI & models

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

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the comic-asset-review-loop — the UPSTREAM ref gate (Phase 1 · S5) skill

What this skill tells your AI

The instructions your AI receives, as published by wanshuiyin/aris-movie-director in skills/comic-asset-review-loop/SKILL.md and read by ahel’s review.

The ref gate the comic side was missing. comic-director's panel_gate reviews composed output; the assembly_gate reviews cross-panel consistency. Neither gates the identity refs that feed both — so without this skill, identity drift is only caught downstream, after credits are spent on a panel bake. This skill is the Layer-2 gate on a SINGLE reusable reference asset (the character sheets / location plates / prop cutouts the ART_BIBLE.md points every bake at), run before any panel composition.

A reusable asset is a one-way door: once a hundred panels condition on duo_canonical_ref_v001.png, you cannot quietly swap it. So the lock must be earned cross-model, never self-certified by the generator. This skill locks an asset only at 准×3 — Claude and Gemini and Codex each independently pass the same review round — or it sends the asset back to be regenerated, or (at the round ceiling) escalates the asset requirement itself back to the outline/storyboard layer. The executor (and the generation model family) never acquit their own ref.

Pipeline position (the Phase-1 asset DAG — the documented contract): this lock runs AFTER the human-approved outline (OUTLINE_DRAFT_VALID only requires every referenced asset to be DECLARED with a complete, generatable request — never pre-locked assets) and the provisional storyboard's consolidated_asset_requests, and BEFORE OUTLINE_FINAL_LOCK (cheap re-check that the locked assets still match the approved outline) → storyboard FINAL asset-resolution validation → blueprint authoring, where the hard locked-asset barrier sits. (The old single-stage contract — outline gate demanding locked assets — deadlocked and is retired.)

  asset (review_status:"pending")  ◀── comic-asset-ref-generator (S4: generates, NEVER self-locks)
        │
        ▼
   LAYER 1 — STATIC CI GATE (deterministic, zero credits)
        check_asset_collisions.py  (one canonical owner per filename — "one visual dialect, never two")
        + build asserts: zero-text contract · literal contract · raster-ref 6-field completeness
        │  pass
        ▼
   LAYER 2 — VISUAL LOOP   (round R = 1 … MAX_REVIEW_ROUNDS=3)
        ① Claude narrative  ‖  ② Gemini visual (primary: identity/iso)  ‖  ③ Codex synth (policy/semantic)
        each reads the REAL asset file · scores 5 dims (0–5 ints) · flags failure_modes from the fixed vocab
        │
        ▼ Codex synthesises: MIN-fuse per dim (max for inverted) · threshold · per-reviewer unanimity
   准×3 ? ── no, a family HARD-FAILS a floor ─▶ comic-asset-ref-generator --regenerate (bump version) ─▶ ① R+1
        │ no, third family just ABSENT (e.g. Gemini down) ─▶ re-VOTE (re-collect the missing reviewer; NO re-bake)
        │ no (safety_ip<3)  ─▶ HARD abandon_shot (IP veto, no retries)
        │ no (R == MAX_REVIEW_ROUNDS) ─▶ ESCALATE the asset REQUIREMENT to outline/storyboard (abandon_shot)
        │ yes
        ▼
   LOCK  — asset.status=locked + payload.review_status=locked  (ONE-WAY DOOR, immutable for the rest of Phase 1)
        + score_progression audit table (ASSET_REVIEW.md) + decision/review/failure_mode wiki nodes + edges

Constants

  • REVIEWERS = Claude (this agent, narrative) ‖ mcp__gemini__analyzeFile (model: "auto-gemini-3", NEVER raw gemini-3.1-pro — 429 silent-downgrade to 2.5) ‖ mcp__codex__codex r1 / mcp__codex__codex-reply r2+ (model_reasoning_effort: "xhigh", save the threadId; pin no model — Codex reviewer calls follow the local codex config, currently gpt-5.6-sol, exactly like the shipped Codex CLI reviewers in run_comic.py, which pass no -m; only the upstream BAKE payload pins gpt-5.5 + xhigh, the run_comic.get_bake_plan() compat default — a config-driven override there is planned, not yet implemented).
  • 准×3 UNANIMITY — the lock predicate. A Codex-synth approve is NOT enough: cc_approves AND gemini_approves AND codex_approves must ALL be true in the same round (each reviewer's own dim scores must independently clear threshold). Codex-approve-without-unanimity is DOWNGRADED to revise (the "准×3 bite"). prior_lock_count = the count of approvals accumulated this round; lock iff prior_lock_count >= 3.
  • MAX_REVIEW_ROUNDS = 3 (hard ceiling). Even if Codex keeps saying regenerate, the loop STOPS at 3 and routes back to outline/storyboard — the requirement changes; the gate does not grind an image forever.
  • CROSS-MODEL ACQUITTAL — Codex is the synth/policy reviewer and (in the broader pipeline) the generation family for raster refs. A Codex approve can diagnose/veto but is never the sole acquitter; the lock needs Gemini approve + Claude approve too (acceptance-gate.md: the loop DRIVES, it can't ACQUIT).
  • SAFETY_IP SINGLE-VOTE VETOsafety_ip < 3 from ANY reviewer = immediate HARD abandon_shot, no retries (generalises comic-director's anatomy single-vote veto to IP/likeness).
  • --lite is NON-ACQUITTING — when Gemini is unavailable it may set only a PROVISIONAL status: under_review (tag lite_provisional: true), NEVER a root status: locked. The one-way LOCK requires the THIRD family (Gemini) to lock-pass in a later round: 准×3 is CC AND Gemini AND Codex same-round unanimity (canon), and the only sanctioned relaxation is the gate's "a model is UNAVAILABLE → threshold-only PROVISIONAL, exit 2" — never a Gemini-absent lock. Use --lite only to keep progress while Gemini is down; never the default; it can never write the locked one-way door.
  • OUTPUT = examples/<project>/story/ASSET_REVIEW.md (the human score-progression audit) + wiki nodes/edges under wiki/nodes/ + per-reviewer trace under .aris/traces/comic-asset-review-loop/.

Input contract / what this skill owns

The contract boundary is the asset wiki node authored by comic-asset-ref-generator (S4) at review_status: "pending" — generated, never self-locked. This skill is pure verify + lock:

  • It never edits the asset's pixels or geometry — on regenerate/revise it routes a ≤140-char repair_instruction back to S4, which bumps the version and emits a new file.
  • It never invents scores or a verdict on behalf of an absent reviewer — a dim no reviewer scored is not_scored and EXCLUDED from the threshold, never substituted with 0 (the v1.0 bug; see Hard do/don't).
  • It writes back only status: "locked" + payload.review_status: "locked" on a 准×3 approve, plus the review/decision/failure_mode trace. locked is the one-way door — immutable for the rest of Phase 1.

Read the asset node payload (per schemas/node_schema.json, node_type: "asset", payload.required = [asset_kind, name, visual_description, identity_lock, ref_requirements, review_status, version]):

  • asset_kind ∈ {character, scene, prop, text_panel, logo_free_symbol} — routes the per-reviewer query (see §"Asset-kind specialisation").
  • identity_lock.{must_preserve[], must_avoid[]} — the per-asset rubric (e.g. from ART_BIBLE.md §1: executor = blue hoodie #1D4684 + brown hair + no beard; reviewer = green hoodie #30582D + near-black hair + beard; "颜色/胡子/体型轮廓不可变").
  • ref_requirements.{aspect_ratio, background (white|transparent), pose, isolated_reference}.
  • output_ref.{file_path, data_url, sha256, width, height, mime} for a raster ref (all 6 fields, else schema violation) OR {generator_script, owner_script, file_sha256} for a deterministic SVG/JSON source.

The EXACT gate (ported from asset-review-loop)

Layer 1 — static CI gate (deterministic, MANDATORY, zero credits)

Run before any visual reviewer call (and re-run before lock). Fail-closed; correctness costs no credits.

  • single-source collision checkcheck_asset_collisions.py static-scans every gen_*.py with the regex \bw\(\s*["']([^"']+\.(?:svg|json|png))["'] and asserts each output filename has EXACTLY ONE generator owner. len(writers) > 1exit 1. This is the tool that enforces "one visual dialect, never two": two generators writing the same path with different content is a silent, run-order-dependent corruption (whoever runs last wins). It is deliberately NOT a "skip if file exists" guard — that would hide the order bug instead of surfacing it. Verdict: exit 0 (clean) is a precondition for entering Layer 2; exit 1 blocks the round and routes to S4 to rename/delete the duplicate. (Verified clean at 26 outputs in the worked example.)
  • zero-text contract — for any asset that must contain no glyphs (e.g. the S22 constellation): assert "<text" not in svg in the emitter. A zero-text asset that contains text fails the round.
  • literal contract — for a deterministic SVG/JSON source, its _contract field forbids re-layout-by-eye; the JSON is the single coordinate truth (e.g. wiki_starmap_nodes_v1.json for S16b labeled + S22 wordless).
  • raster 6-field completeness — a raster output_ref has all of file_path, data_url, sha256, width, height, mime; sha256 matches the file on disk (a partial write is a schema violation, not a lockable ref).

Layer 2 — the 5-dim visual rubric (0–5 integers each)

Each reviewer reads the real asset file (image bytes via Read / analyzeFile) and scores all five; the Primary reviewer's score is authoritative for that dim, the Secondary corroborates.

  1. identity_lock_satisfied — preserves EVERY must_preserve trait, avoids EVERY must_avoid. Primary = Gemini (visual), Secondary = Codex (semantic). character → face/age/hair/costume/beard; scene → layout + lighting-type + era.
  2. ref_quality — sharp, well-framed, correctly exposed, artefact-free. Primary = Gemini, Secondary = Claude. (text_panel → glyph crispness weighted.)
  3. bg_isolation — background is EXACTLY the required colour, isolated, no halo / shadow-spill / other subjects. Primary = Gemini ONLY. For a scene this dim becomes scene_emptiness (no characters or props sneaked into a location plate), same 0–5 scale.
  4. reuse_readiness — compositable into multiple downstream conditions without per-shot rework. Primary = Claude (usage_fit), Secondary = Gemini (composability).
  5. safety_ip — no celebrity likeness / copyrighted character / brand logo. HIGHER = SAFER (5 = clean). Primary = Codex (policy), Secondary = Gemini (recognition).

EXACT verdict thresholds (Codex synth) + the 准×3 bite

These are THIS skill's pre-gate routing thresholds (the asset-gate step-3 routing this skill OWNS), and they are written to MATCH comic-cross-layer-gate's --gate asset LOCK predicate verbatim. The gate's LOCKED condition is identity_lock_satisfied >= 4 AND ref_quality >= 4 AND bg_isolation >= 4 AND safety_ip >= 4 AND the 准 ×3 same-round cross-model unanimity rule — reuse_readiness is ADVISORY at the gate and does not block the lock. The approve rule below uses exactly those four floors; reuse_readiness only feeds revise/regenerate routing, never blocks the lock.

Codex MIN-fuses each dim across the reviewers who scored it (the gate's deterministic rule: min per dim, max for an inverted dim, single scorer → use it, NO scorer → not_scored EXCLUDEnever 0-substitute, never average: averaging {5,3}→4 would slip a sub-floor ref past the >=4 lock; min=3 correctly fails). So this inline routing can only be stricter, never weaker, than --gate asset. The legal asset verdict set is {approve, regenerate, locked, abandon_shot} (gate-enforced); the internal revise below is an alias that routes a regenerate. Then:

  • approveidentity_lock_satisfied >= 4 AND ref_quality >= 4 AND bg_isolation >= 4 AND safety_ip >= 4 (the four gate floors). asset_kind == "character" override: identity_lock_satisfied == 5 is REQUIRED (a raster character ref locks only at a perfect identity score — 4 is not enough for a face hundreds of panels condition on); the other three floors stay >= 4. (准×3 same-round unanimity still applies on top — see the LOCK note below.)
  • regenerateidentity_lock_satisfied < 4 (or < 5 for a character) OR bg_isolation < 4 OR ref_quality < 4 OR safety_ip ∈ [3, 4) OR reuse_readiness < 3 (advisory: routes a regen hint, does not itself block a lock that already clears the four floors).
  • revise ← mid-band, a single targeted edit likely fixes it (softer hint, still routes a regenerate with a focused repair_instruction).
  • abandon_shotsafety_ip < 3 (HARD STOP, IP) OR round >= MAX_REVIEW_ROUNDS without convergence.

Deterministic-SVG / JSON asset branch (a parametric builder, NOT a raster ref → the visual 5-dim rubric does not apply). A deterministic-SVG source locks iff ALL FOUR hold (Layer-1 asserts + a 3-dim 0–5 read): single_source_owner == pass (Layer-1 check_asset_collisions.py exit 0 — exactly one generator owns the filename) AND render_preview_legible >= 4 (the SVG/JSON rasterizes to a non-empty, on-spec preview PNG — a RAW render pre-check, not a vote) AND literal_contract >= 4 (the _contract field is honored: literals are ascii-tokenizable, the JSON is the single coordinate truth, no eyeball re-layout) AND style_family_consistency >= 4 (one visual dialect — palette/stroke/grid match the ART_BIBLE.md family). Plus the zero-text / non-gated-text asserts: a wordless source contains no <text> (assert "<text" not in svg); a labeled source's glyphs are exactly the contract's literals. Same 准×3 same-round unanimity gates the SVG lock as the raster lock. (Gemini PRIMARY on render_preview_legible + style_family_consistency; Codex PRIMARY on literal_contract; single_source_owner is the Layer-1 tool, not a reviewer vote.)

LOCK requires 准×3 UNANIMITY, not Codex-synth-approve. Even on a Codex approve, the lock fires iff cc_approves AND gemini_approves AND codex_approves in the same round (each reviewer's own 5 scores independently clear the per-reviewer thresholds — including the identity_lock_satisfied == 5 floor for a character ref / the four-dim SVG floors for a deterministic source). A Codex approve with any reviewer below threshold is DOWNGRADED to revise. (--lite is non-acquitting: with Gemini absent it may only write provisional status: under_review + lite_provisional: true, and can NEVER satisfy the LOCK predicate or reach P6.) This same-round unanimity rule is exactly comic-cross-layer-gate's 准 ×3 for --gate asset (this skill is the OWNER of that convention) — NOT "three consecutive rounds".

Veto rules (hard, non-negotiable)

  • safety_ip < 3 from ANY reviewer → immediate abandon_shot (single-vote IP veto, no retries).
  • Layer-1 fail (collision / missing literal / partial raster / zero-text breach) → block the round, route to S4, do not enter Layer 2.
  • round >= MAX_REVIEW_ROUNDS without 准×3 → ESCALATE the asset requirement to comic-outline-creator / comic-storyboard-creator (the spec/outline must change; stop re-baking the image). Mutual recursion is bounded by this ceiling.

Controlled failure-mode vocabulary (the FIXED set reviewers draw tags from)

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. Every reject mints a failure_mode wiki node fail:<asset-slug>_<tag> tagged from this set (the <tag> must match [a-z0-9_]+ — strip any stray punctuation before assembling the node_id; maps to comic-director's anatomy/identity vetoes) and routes its tag into the S4 repair_instruction. The node MUST carry the schema-required shape (copy the fail:* skeleton in "Wiki node skeletons" below verbatim): root status: "active" + payload {layer, affected_shot_ids, active, repair_pattern}. Because this is an upstream asset gate with no shots yet, affected_shot_ids is [] (empty) — never invented. NB the shipped example failure nodes are FILES named fail_*.json (the colon lives only inside the node_id, e.g. fail:s09_a01) and are ALL downstream shot-level nodes (layer:"panel_visual", non-empty affected_shot_idsfail_s09_a01.json carries ["S09"]); no asset_ref-layer example exists in the repo, so do NOT copy the example nodes for this gate — the skeleton below is the authoritative shape.

Wiki node skeletons (the EXACT shape each minted node MUST carry — validated by cli/validate_wiki.py)

Every node this skill writes obeys schemas/node_schema.json: the root carries node_id, node_type, title, status, created_at, payload (all six required), the node_id matches ^(…|review|decision|fail):[a-z0-9_-]+$, and the payload carries every field in validate_wiki.py PAYLOAD_REQUIRED for that type. The canonical root status for each runtime type (schema $comment; the real example nodes carry exactly these): review → "complete", decision → "final", failure_mode → "active". Copy these skeletons verbatim:

// review:*  — one per reviewer per round (P1 cc, P2 gemini, P3 codex). node_id: review:<asset-slug>_<reviewer>_round{R}
// PAYLOAD_REQUIRED["review"] = [target_node_id, reviewer, gate_kind]  (review_scores carries the 5 dims)
{ "node_id": "review:duo_canonical_ref_v001_cc_round1", "node_type": "review", "status": "complete",
  "title": "CC narrative asset review duo_canonical_ref v1 r1", "created_at": "<ISO-8601>",
  "payload": { "target_node_id": "asset:<asset-slug>", "reviewer": "cc", "gate_kind": "asset",
               "review_scores": { "identity_lock_satisfied": 5, "ref_quality": 4, "bg_isolation": 4,
                                  "reuse_readiness": 4, "safety_ip": 5 },
               "failure_modes_flagged": [], "timed_out": false } }

// decision:*  — one per round after the synth+准×3 (P4). node_id: decision:asset_<asset-slug>_round{R}
// PAYLOAD_REQUIRED["decision"] = [target_node_id, verdict, gate_kind]
{ "node_id": "decision:asset_duo_canonical_ref_v001_round1", "node_type": "decision", "status": "final",
  "title": "asset_gate duo_canonical_ref v1 r1 -> locked", "created_at": "<ISO-8601>",
  "payload": { "target_node_id": "asset:<asset-slug>", "gate_kind": "asset", "verdict": "approve",
               "reasoning": "LOCK 准×3 — identity=5 ref=4 bg=4 safety=5 reuse=4; cc&gemini&codex approve r1",
               "repair_instruction": "", "prior_lock_count": 3 } }

// fail:*  — only on a reject/abandon_shot (P0 Layer-1 fail, P2 parse failure-mode, P5 reject). node_id: fail:<asset-slug>_<tag>
// PAYLOAD_REQUIRED["failure_mode"] = [layer, affected_shot_ids, active]   (upstream gate → affected_shot_ids: [])
{ "node_id": "fail:duo_canonical_ref_v001_costume_drift", "node_type": "failure_mode", "status": "active",
  "title": "asset_gate duo_canonical_ref costume_drift", "created_at": "<ISO-8601>",
  "payload": { "layer": "asset_ref", "affected_shot_ids": [], "active": true,
               "repair_pattern": "executor 蓝衣棕发无须 (must_preserve); 当前绿衣漂移 — 重生为 #1D4684 hoodie" } }

After any lock (P6), run python3 cli/validate_wiki.py examples/<project> — it MUST PASS (root status enum, node_id form, payload invariants). A node missing root status or target_node_id / layer / affected_shot_ids / active is the exact failure the validator catches.

The two fail-closed engine contracts (where they bite here)

This is an upstream gate, so it enforces the preconditions the downstream engine fail-closes on. Do NOT conflate the three SVG fields (per schemas/node_schema.json + the engine): the wiki blueprint.payload.content_svg (top-level on the blueprint payload), the panel_spec.payload.content_blueprint (the panel_spec's own field — there is NO content_svg on a panel_spec), and the comic.json panel's runtime condition.content_svg (the field the engine reads at spiral_engine.js .condition.content_svg).

  1. Every panel needs a renderable blueprint SVG. A locked asset is only useful if a panel can condition on it — so when locking a deterministic SVG/JSON source, verify it rasterizes to a non-empty PNG (a blueprint node would carry it as blueprint.payload.content_svg), because at bake time the engine refuses a comic.json panel whose condition.content_svg is null/absent. A ref that cannot become a content-SVG condition is not "reuse-ready" — score reuse_readiness (and, for an SVG source, render_preview_legible) accordingly. Never condition.content_svg: null.
  2. A baked figure-panel needs condition.expected_literals. If the asset is a text_panel / figure source whose downstream panels bake numbers (e.g. a wandb curve, a verdict stamp, a DDL widget), its lock MUST verify the literal contract holds (the literals are ascii-tokenizable and the JSON _contract forbids eyeball re-layout) — because comic-director's gate refuses to run a baked figure-panel whose comic.json condition.expected_literals is empty. Locking a figure source that cannot supply gateable literals would strand the panel; a scene panel with no audited numbers takes text_mode: "html" instead.

Numbered procedure (per round R, start R = 1, loop until 准×3 or R = 3)

P0 — Resolve + Layer-1 gate. Resolve the target asset node (single asset_id, or fan out one branch per asset under --batch-from-outline). Assert review_status == "pending"; abort if already locked (one-way door). Route by asset_kind (visual raster kinds → the raster 5-dim branch; a deterministic SVG/JSON source → the SVG 4-dim branch; an audio kind would take the audio branch — out of scope for the comic). Run Layer 1 (check_asset_collisions.py + build-asserts + raster 6-field check); on any failure, write a failure_mode node (root status: "active", payload {layer:"asset_ref", affected_shot_ids:[], active:true, repair_pattern: <the Layer-1 tag-derived hint>} per the fail:* skeleton) + a failure_of edge fail:<…> → asset:<asset-slug>, route to S4, and stop — do not spend a visual reviewer call.

P1 — Claude narrative review. Read the PNG (image bytes). Score all 5 dims (Claude PRIMARY on reuse_readiness; Codex is PRIMARY on safety_ip per the rubric), emit failure_modes_flagged[] (from the fixed vocab) + narrative_notes. Write a review-stage scratch JSON and a wiki review node review:<asset-slug>_cc_round{R} with root status: "complete" and payload carrying the three PAYLOAD_REQUIRED fields target_node_id: "asset:<asset-slug>", reviewer: "cc", gate_kind: "asset" plus review_scores keyed by the 5 dims (see the review:* skeleton above). Append a reviews edge review:<…> → asset:<asset-slug> (review → target — the direction the gate walks; NEVER reviewed_by). Atomic write (.tmpmv).

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
61
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
comic-asset-review-loop
Source
github.com/wanshuiyin/aris-movie-director