lovstudio-maintain-partners — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited lovstudio-maintain-partners (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Maintains the configured website repo. Resolve the path from --repo, LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, or the shared user profile. The partners strip usually lives in app/(main)/(home)/PartnersGrid.tsx as a PARTNERS: Partner[] array; older sites may still keep it in app/(main)/(home)/WorkshopDispatch.tsx. Logos live in public/partners/<slug>/logo.png; taglines in src/i18n/messages/{zh-CN,en,ja,th}.json under dispatch.partner*Tagline.
Before touching files, resolve:
SKILL_ROOT="${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}"
SKILL_DIR="${SKILL_DIR:-$SKILL_ROOT/lovstudio-maintain-partners}"
WEB_ROOT="${LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT:?Set this or pass --repo}"
PARTNERS_TSX="${LOVSTUDIO_MAINTAIN_PARTNERS_FILE:-app/(main)/(home)/PartnersGrid.tsx}"Use this precedence for the website root:
--repo <path> on add_partner.py / audit_partners.py.LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT.${LOVSTUDIO_SKILLS_PROFILE:-$HOME/.lovstudio/skills/profile.json}.
LOVSTUDIO_WEB_ROOT and PARTNERS_SITE_ROOT are accepted as legacy aliases, but should not be the public contract for reusable skills.
Use this precedence for the partners TSX file:
--partners-file <path>.LOVSTUDIO_MAINTAIN_PARTNERS_FILE.sites.partners_file, lovstudio.partners_file,partners.file, or workspace.partners_file.
app/(main)/(home)/PartnersGrid.tsx, then legacyapp/(main)/(home)/WorkshopDispatch.tsx.
LOVSTUDIO_PARTNERS_FILE and PARTNERS_FILE are accepted as legacy aliases, but should not be the public contract for reusable skills.
For details and supported profile keys, read references/user-config.md.
lovstudio-find-logo is required for all logo discovery. This skill mustnot scrape homepages itself or keep a separate fallback crawler.
depends_on frontmatter field to declare skill-level dependencies.This mirrors the depends_on field in lovstudio-general-skills/skills.yaml; unknown frontmatter keys are expected to be ignored by agents that do not consume dependency metadata.
(light grayscale, CSS height: 32px ≈ 2.5× density, sharp enough), 240px for event posters or any retina export at scale: 2 or higher.
the light grayscale strip.
--show-namewhen adding so the brand name renders next to the icon.
<品牌名> · <一句话定位> in Chinese; mirror style in en/ja/th.AskUserQuestion.lovstudio-find-logo: python3 "$SKILL_ROOT/lovstudio-find-logo/scripts/find_logo.py" \
--name "<显示名>" --url <URL> --slug <slug> --jsonUse the archived primary asset under ~/.lovstudio/logo-collection/<slug>/logo.<ext>. If find_logo.py returns no candidates, stop and ask the user for a better official URL / press-kit URL, then rerun find_logo.py. Do not call a local scraper from this skill.
rsvg-convert -h 240 ~/.lovstudio/logo-collection/<slug>/logo.svg \
-o /tmp/<slug>-raw.pngUse the rasterized /tmp/<slug>-raw.png as --src. For non-SVG sources, use the archived primary asset directly.
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src <archived-or-rasterized-logo> \
--dst "$WEB_ROOT/public/partners/<slug>/logo.png" \
--invert auto python3 "$SKILL_DIR/scripts/add_partner.py" \
--repo "$WEB_ROOT" \
--partners-file "$PARTNERS_TSX" \
--name "<显示名>" --href "<URL>" \
--logo "/partners/<slug>/logo.png" \
--key partner<Slug>Tagline \
--category community \
--zh "..." --en "..." --ja "..." --th "..." \
[--show-name]python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src public/partners/<slug>/logo.png \
--dst public/partners/<slug>/logo.png \
--invert autoRe-read after to verify.
Ask for the source file path directly, or read it from the user's configured workspace/profile. Do not assume a private partners folder.
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src "<user-provided path>" \
--dst "$WEB_ROOT/public/partners/<slug>/logo.png" \
--invert autoJPEG inputs auto-strip near-white background to transparent before crop.
python3 "$SKILL_DIR/scripts/audit_partners.py" \
--repo "$WEB_ROOT" \
--partners-file "$PARTNERS_TSX"
# add --probe to also HTTP-check every href (slow, requires proxy)Reports: missing logo files, missing i18n keys per locale, dead URLs.
When: putting 3+ partner logos in a single horizontal strip and they look different sizes despite having the same CSS height. Common in event posters, hero sections, "联办 / co-host" rows.
Root cause: each source file has different internal padding (designer canvas margin), so two PNGs both set to height: 24px render at different visible heights because their content occupies different fractions of the canvas. Per-logo CSS height tweaks based on eyeballed content ratios are unstable—different displays / scaling will diverge again.
Reliable fix — trim at file level, uniform CSS box:
target is 240px (3× density for retina poster export at scale: 2; 80px gives only 1.7× and looks soft after PNG export). Use --invert off if the source is already light-on-transparent (don't double-invert):
for f in lujiazui juanyi citic-bookstore citic-thinker-lab; do
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src "<configured-partners-source>/<brand>/<file>.png" \
--dst <event-assets>/partners/$f.png \
--height 240 --invert auto
doneAlways normalize from the original source, never from a previously normalized 80px file (upscaling = blurry — burned by this on juanyi).
normalize_logo.py operates onraster pixels and cannot crop SVG viewBox padding. Without this step an SVG always renders smaller than rasterized PNG siblings:
rsvg-convert -h 720 brand.svg -o /tmp/brand-raw.png # 3× of 240
python3 "$SKILL_DIR/scripts/normalize_logo.py" \
--src /tmp/brand-raw.png --dst <event-assets>/partners/brand.png \
--height 240 --invert offrsvg-convert ships with librsvg (brew install librsvg).
rounded square — common in app-icon-style SVGs from find-logo), strip the background before rasterizing, otherwise filter brightness(0) invert(1) flattens it into a solid white block that hides the icon:
# Drop the outer <rect fill="#000"...> wrapper
sed -E 's|<rect[^/]*fill="#0+"[^/]*/>||' brand.svg > /tmp/brand-clean.svg
rsvg-convert -h 720 /tmp/brand-clean.svg -o /tmp/brand-raw.png <span class="ps-logo-box"><img src="..." class="ps-logo"></span> .ps-logo-box {
width: 96px; height: 30px; /* fixed grid cell */
display: inline-flex;
align-items: center; justify-content: center;
border: 1px solid rgba(255,255,255,0.10);
border-radius: 4px;
padding: 3px 6px;
box-sizing: border-box;
}
.ps-logo { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }Fixed boxes give a stable matrix look — narrow logos (icon-only) and wide logos (icon + wordmark) all occupy the same footprint, with the asset scaled to fit. Auto-width flex (the older recipe) makes per-row total widths unpredictable as logos get added/removed.
(e.g. event poster), most brand logos are designed for white BG and look inconsistent (some have black text, some have brand-colored marks). The stable recipe:
.ps-logo { filter: brightness(0) invert(1) opacity(0.88); }
/* logos already white-on-transparent — opt out of inversion */
.ps-logo.ps-logo-original { filter: opacity(0.88); }brightness(0) flattens all colors to black, then invert(1) produces uniform white at the configured opacity. The .ps-logo-original escape hatch is for source files that are already white-on-transparent (white SVG variants from a brand kit) so you don't double-process them into invisible black-on-dark.
has an icon (no "BrandName" wordmark beside it), don't ship just the icon in a 96×30 box (it'll look like an unidentified mark). Compose the wordmark with PIL using the brand's own font when possible:
from PIL import Image, ImageDraw, ImageFont, ImageOps
# 1. rasterize cleaned SVG, invert white→black so default filter works
icon = Image.open('/tmp/brand-icon.png').convert('RGBA')
r, g, b, a = icon.split()
inv = Image.merge('RGB', (ImageOps.invert(r), ImageOps.invert(g), ImageOps.invert(b)))
icon = Image.merge('RGBA', (*inv.split(), a))
icon = icon.crop(icon.getbbox())
target_h = 240
icon = icon.resize((int(icon.width * target_h / icon.height), target_h), Image.LANCZOS)
# 2. render wordmark in brand font (find-logo bundles fonts/ when found)
font = ImageFont.truetype('partners/<brand>/fonts/<Family>.ttf', 150)
# 3. compose icon + gap + text on transparent canvasThe PNG goes through the same brightness(0) invert(1) filter as raster logos — match colors with all other entries automatically. Use the brand's own font (often shipped under <brand>/fonts/ by the find-logo skill); fall back to system SF / Helvetica only if no brand font is available.
heights like .ps-logo-juanyi { height: 26px }. It's brittle (every new logo needs another magic number), unstable across browsers, and breaks the moment a designer reships the source asset with different padding.
| Flag | Default | Notes |
|---|---|---|
--src | required | input image (PNG/JPG/rasterized SVG) |
--dst | required | output PNG path; parent dirs auto-created |
--height | 80 | target content height. Use 240 for retina poster export (scale: 2) — 80 looks soft after 2× downscale. |
--invert | auto | auto / off / full / selective (selective preserves colored icons) |
| Flag | Notes |
|---|---|
--repo | website repo root; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, profile JSON, or legacy LOVSTUDIO_WEB_ROOT / PARTNERS_SITE_ROOT |
--partners-file | PARTNERS TSX file; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_FILE, profile JSON, legacy LOVSTUDIO_PARTNERS_FILE / PARTNERS_FILE, PartnersGrid.tsx, or WorkshopDispatch.tsx |
--name | display name (CJK ok) |
--href | brand URL |
--logo | path under /public, e.g. /partners/foo/logo.png |
--key | i18n key, e.g. partnerFooTagline |
--category | compute / peer / invest / media / community; default community |
--zh / --en / --ja / --th | tagline strings (all required) |
--show-name | render name next to icon for narrow logos |
| Flag | Notes |
|---|---|
--repo | website repo root; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_SITE_ROOT, profile JSON, or legacy LOVSTUDIO_WEB_ROOT / PARTNERS_SITE_ROOT |
--partners-file | PARTNERS TSX file; defaults to LOVSTUDIO_MAINTAIN_PARTNERS_FILE, profile JSON, legacy LOVSTUDIO_PARTNERS_FILE / PARTNERS_FILE, PartnersGrid.tsx, or WorkshopDispatch.tsx |
--probe | HTTP-probe every href (slow, needs proxy env vars) |
Sandbox child processes don't inherit the system ClashX proxy. Before fetching logos with lovstudio-find-logo or probing partner URLs, export:
export https_proxy=http://127.0.0.1:7890 \
http_proxy=http://127.0.0.1:7890 \
all_proxy=socks5://127.0.0.1:7891audit_partners.py already injects these for curl invocations.
git clone https://github.com/lovstudio/find-logo-skill \
"${LOVSTUDIO_SKILLS_INSTALL_DIR:?Set LOVSTUDIO_SKILLS_INSTALL_DIR}/lovstudio-find-logo"
python3 -m pip install Pillow
brew install librsvg # for SVG logo sources~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.