Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

icon-loop

License: Apache 2.0

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.

Why this exists

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.

Pipeline

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
Loading

Requirements / build

  • Rust toolchain
  • claude CLI on PATH (default backend, subprocess — no separate API key needed)
  • optional: OPENROUTER_API_KEY for critic-panel diversity (see below) — without it, openrouter-assigned critics fall back to claude and the run logs why
cargo build --release
# target/release/iconloop

Commands

design

Run a fresh round from a spec.

iconloop design --spec specs/default.toml --out runs/v1 --concurrency 3

Output:

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

refine

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

validate

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/v1

Persona pool

specs/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.

Why discourse is independent and blind, not sequential debate

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_note in 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

--prior state

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.

Validated on real runs

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_backends runs 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 fixing discourse.rs to build the attached-image order from the same shifted list 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.

Limitations

  • The apparent improvement from refine hasn'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-critique to 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_backends list from silently degrading back to one provider if OPENROUTER_API_KEY isn't set (the run still succeeds, just with a logged warning and the critic_diversity_note in the report).
  • Regressive over-correction is only partially guarded against. dead_concepts labels 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.

Lineage

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

About

app icon design loop — lens→render→policy→discourse→quantify (Loop-Suite pattern)

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages