A Rust CLI that designs app icons through independent persona generation → deterministic render/policy gates → fully independent blind discourse → deterministic verdict, instead of hand-iterating image-generation prompts and eyeballing the result.
Iterating on an icon by re-prompting an image model and looking at the PNG has three recurring failure modes this tool exists to catch mechanically instead of by eye:
- Geometry drifts outside the canvas or collapses to a smear at real icon sizes (29px), and you only notice after the fifth regeneration.
- The maker can't see their own blind spot. A shape intended to read as one thing can read as something else entirely to a fresh viewer — and the person who designed it, or a single LLM asked to grade its own output, is structurally bad at catching that (see Validated on real runs below for a concrete case).
- A "panel" of critics that's actually the same model called three times isn't a panel. If the judges are correlated, N of them collapse to far fewer effective, independent votes.
flowchart TD
A[spec.toml: palette, canvas, personas] --> B[lens.rs: independent per-persona SVG generation]
B --> C[render.rs: resvg renders 1024/180/60/29px]
C --> D[policy.rs: deterministic gates]
D -->|containment / palette / legibility fail| B
D -->|PASS| E[anonymize: copy to candidate_N filenames]
E --> F[discourse.rs: N critics, fully independent, blind]
F -->|critic_backends mixes claude/openrouter| F
F --> G[quantify.rs: deterministic Borda count]
G --> H[collusion warning + minority-opinion surfacing]
H --> I[state.json + report.md]
I -->|refine --prior| J[dead_concepts + prior SVG/critique injected]
J --> B
- Rust toolchain
claudeCLI onPATH(default backend, subprocess — no separate API key needed)- optional:
OPENROUTER_API_KEYfor critic-panel diversity (see below) — without it,openrouter-assigned critics fall back toclaudeand the run logs why
cargo build --release
# target/release/iconloopRun a fresh round from a spec.
iconloop design --spec specs/default.toml --out runs/v1 --concurrency 3Output:
runs/v1/
├── report.md # ranking, deterministic gate results, raw critic text, minority opinions, warnings
├── state.json # full state for refine --prior
├── render/ # per-candidate 1024/180/60/29px PNGs
└── blind/ # the anonymized renders actually shown to discourse
Feed the previous round's SVG and critique text back into the next round's generation prompt.
Requires a fresh --out — never overwrites a prior run.
iconloop refine --spec specs/default.toml --prior runs/v1 --out runs/v2--no-critique: an ablation switch. Drops the per-persona "here's your last SVG and what critics
said about it" block while keeping the shared dead_concepts memory. Compare a run with and
without this flag to check whether critique text is actually doing anything, or whether apparent
"improvement" between rounds is just regeneration noise (see Limitations).
Re-run the deterministic policy gates on a saved state.json — no LLM calls. Useful after
changing the spec's palette or margins.
iconloop validate --spec specs/default.toml --run runs/v1specs/default.toml ships three example personas, each given a genuinely different structural
constraint rather than just a different temperature — generic "be more creative" prompting
regresses to the same statistical-average look regardless of persona label:
| id | persona | constraint |
|---|---|---|
glyph |
Ancient Glyph Carver | reinterpret shared divination visual vocabulary (e.g. stacked bars) geometrically, never a literal copy |
negspace |
Negative-Space Minimalist | one thick, safe outer silhouette + one negative-space cut, nothing else |
crystal |
Crystal Facet Sculptor | a compact, closed polyhedron — facets never extend past the outer silhouette |
Swap these for your own brief in specs/default.toml.
This is the one place icon-loop's shape diverges from codereview-loop's original
AGREE/CHALLENGE/CONNECT/SURFACE discourse. Critics never see each other's rulings; each one gets
the same anonymized image set (with a per-critic cyclic shift of listing order to cancel out
position bias) and answers independently. quantify.rs then aggregates with a plain Borda count —
no LLM in that step — and separately:
- flags unanimous agreement as a thing to double-check, not just celebrate (100% critic
agreement can mean the panel is genuinely right, or that it's correlated/colluding — the
critic_diversity_notein the same report tells you which is more likely) - surfaces minority opinions: a candidate a single critic ranked first but that lost the overall vote, so a strong idea only one judge liked doesn't just vanish into the aggregate
Round state carries forward as free text, not a fixed enum — dead_concepts accumulates losing
candidates' concrete flaws (with which critic/provider raised each one) across every round, so the
next round's personas don't repeat a shape that already failed. This is closer to
research-loop's evidence-accumulation pattern than to codereview-loop's
FIXED/STILL_OPEN/UNKNOWN reconciliation, because icon candidates are regenerated wholesale each
round rather than patched.
Several real rounds against a live example brief, not a synthetic fixture:
- Round 1 (
design): a persona self-reported its own triangle-with-eye design would not be misread. All three blind critics independently read it as a mountain/spark shape instead of the intended concept — a mismatch the deterministic gates (containment, palette, legibility) all passed cleanly, because none of them can check what a shape looks like. This is the concrete case for why blind discourse is a separate stage from policy checks, not a superset of them. - Round 2 (
refine): feeding that critique back in changed the losing persona's next design entirely (triangle+eye → pentagon+crescent), and the new specific flaw the critics found (read as a map pin) was strictly milder than the one before it. - A real bug this caught in itself: the first mixed-
critic_backendsruns looked like clean evidence that panel diversity produces genuine disagreement — an OpenRouter critic ranking things differently than the Claude critics. It wasn't genuine. OpenRouter's request carried the attached images in one fixed order while the prompt's text catalog listed candidate IDs in a position-bias-cancelling shifted order (Claude CLI never had this problem — it opens files by path via the Read tool, so text and image are always the same reference). The OpenRouter critic was silently grading candidate A's image under candidate B's label, one position off, every time. Re-running after fixingdiscourse.rsto build the attached-image order from the sameshiftedlist as the text catalog produced a real, checkable-by-eye-match disagreement instead: on a monochrome variant of the brief, Claude critics ranked an eye design first while the OpenRouter critic's sole point of disagreement put a facet-cut crystal design first — and this time every critic's description of "candidate X" actually matches candidate X's rendered image. That's the actual evidence panel diversity isn't cosmetic; the earlier apparent evidence was an artifact.
- The apparent improvement from
refinehasn't been causally isolated. Round-over-round gains could be the injected critique text working as intended, or could just be regeneration variance — a fresh sample from the same persona might have scored similarly with no feedback at all. Use--no-critiqueto compare before trusting a specific round-to-round delta. - A 3-critic panel with 2 providers is still a small panel. The literature on judge correlation
suggests provider diversity matters more than critic count, but two providers is a floor, not a
ceiling — nothing stops a
critic_backendslist from silently degrading back to one provider ifOPENROUTER_API_KEYisn't set (the run still succeeds, just with a logged warning and thecritic_diversity_notein the report). - Regressive over-correction is only partially guarded against.
dead_conceptslabels each flaw with which critic/provider raised it so a persona can weigh corroborated vs. single-critic complaints, but nothing enforces a hard consensus threshold before a flaw gets fed forward. - Deterministic gates check geometry and pixels, not taste — a design can pass every gate and still be generic or ugly. That judgment stays with discourse and, ultimately, whoever reads the report.
Code-Review-Loop (original) → codedesign-loop (discourse ported to pre-code design review) →
icon-loop (ported to icon design; discourse itself reshaped from sequential debate to
independent blind evaluation + deterministic aggregation — see above for why).