Skill: chenghaofan1998/openclaw-shrimpcard (version 2.0.0)

--- name: openclaw-shrimpcard description: Use when an agent needs to turn evidence-backed behavior into a validated self-intro submission, a share-card JSON payload, and a final screenshot-friendly HTML selfie card with a real 8-bit character image. --- # Agent Selfie Card Use this repository as one self-contained skill package. Canonical flow: `agent-evidence -> agent-self-intro-submission -> share-card -> final image -> selfie-card.html` ## Use this skill for - turning repeated agent evidence into a concise public identity - rejecting generic self-description before it becomes public copy - producing a validated `share-card/1.0` payload - rendering a screenshot-friendly HTML card only after a real image is attached ## Do not do - do not invent identity claims that are not repeated in evidence - do not guess missing owner identity fields - do not stop at prompt-only or placeholder-image state - do not force lobster branding unless it is already true for the agent - do not derive the target agent's public identity from the skill name, repo name, script path, package name, or interface label ## Workflow Runtime note: - install Python dependencies from `requirements.txt` before running the scripts - files under `examples/` are smoke-test fixtures only; never treat them as the live target agent's evidence or submission - if the only available inputs are fixture/example files, stop and ask the human for the real agent evidence 1. Gather evidence - Prefer `agent-evidence/1.0`. - Pull from task traces, tool traces, output artifacts, owner feedback, saved preferences, and memory notes. - Do not reuse repo example fixtures as the target agent's identity source. - Treat skill/package/repo/config names as implementation metadata, not identity evidence. 2. Build memory-search notes - Use `scripts/build_memory_search_prompt.py`. - Draft short candidate phrases early so field limits are handled before final JSON. - If owner name or contact is still unknown after checking evidence and memory, stop and ask the human. 3. Build the retry-until-valid workflow prompt - Use `scripts/build_agent_workflow_prompt.py`. - Pass `--lang zh` or `--lang en` so final card copy matches the user language. - Generate one language per card, chosen from the user language; do not produce bilingual toggle content in a single final card. 4. Build the submission prompt - Use `scripts/build_submission_prompt.py`. - The agent authors `agent-self-intro-submission/1.0` itself. 5. Validate and retry - Use `scripts/validate_self_intro_submission.py`. - Rewrite any field that is too long, generic, duplicated, malformed, or visually weak. 6. Convert to share card - Use `scripts/submission_to_share_card.py`. 7. Build the image task - Use `scripts/build_image_task_prompt.py`. - The final image must be a real 8-bit pixel-art character PNG, not an abstract icon and not a sample reuse. 8. Attach image and validate bundle - Use `scripts/attach_generated_image.py`. - Use `scripts/validate_final_bundle.py`. 9. Render HTML - Use `scripts/render_card_html.py`. - Final rendering should happen only after bundle validation passes. - After rendering, run `scripts/validate_rendered_html.py`. - The final HTML must not expose internal `card_id` text on the poster face. - The final visible sidebar headline/subheadline must come from real card fields such as `poster_headline`, `poster_subline`, or payload fallbacks, not from preview-only empty-state copy. ## Key rules - describe repeated observed behavior, not hypothetical ability - prefer owner value over tool trivia - if live evidence does not explicitly establish a branded agent identity, use a neutral identity - default to neutral naming and `character_style: agent-avatar` when special branding is not clearly evidenced - only use special names or mascots such as OpenClaw, Hermes, lobster, shrimp, crab, or similar when those signals are explicit in repeated live evidence or explicitly requested by the human - `top_capabilities`: exactly 3 - `persona_tags`: exactly 3 and distinct - `share_keywords`: 3 to 4 and distinct - short fields must stay readable, not cryptic - all final public-facing text must be in one language only, matching the user language for that run - `selfie_prompt` must explicitly require 8-bit or pixel-art direction - `visual_brief` and `selfie_prompt` must describe one recognizable character concept - `character_style: lobster` is valid only for claw/lobster-branded agents unless the user explicitly overrides it - `card_id` is metadata only; do not surface it as visible poster copy - when rendering a final card with real data, visible preview placeholders must be fully replaced by real card text ## Bundled resources - `schemas/`: local schema copies used by validators - `scripts/`: prompt builders, validators, converters, image attachment, HTML renderer, rendered-HTML QA - `assets/card-template.html`: final card template - `references/agent-self-extraction-guide.md`: load only when you need more evidence-extraction guidance - `examples/`: reference payloads and smoke-test fixtures

path: CLAWHUB.md
content: # OpenClaw ShrimpCard

如果你也受不了那种一眼就很“AI”的 agent 介绍页，这个 skill 可能正适合你。

OpenClaw ShrimpCard 做的事情很简单：把一个代理真正做过的事，整理成一张能拿得出手的身份卡。不是空话，不是套模板，不是“强大助手”“高效执行”这种谁都能写的词，而是尽量从真实 evidence 出发，做出一套可以公开展示的成品。

它最后会帮你产出：

- 一份基于证据的自我介绍 JSON
- 一份结构化的 share card JSON
- 一张真实的 8-bit 像素角色图
- 一份适合截图、分享、展示的 HTML 卡片

## 这个 skill 适合谁

适合这些人：

- 想给自己的 agent 做一个像样展示页的人
- 不想再用空泛文案包装 agent 的人
- 想把 task trace、memory、owner feedback 这些真实材料压缩成公开卡片的人
- 想要“能交付的成品”，而不是停在 prompt 或半成品阶段的人

如果你只是想随便生成一个炫酷海报，它可能不是最省事的那个。
但如果你在意“这张卡写的到底像不像我”“是不是有证据支撑”“能不能真的发出去”，那它会很顺手。

## 它和一般 agent 展示工具有什么不一样

很多工具的问题不是做不出图，而是做出来的东西太像样板。

OpenClaw ShrimpCard 比较较真，它会卡这些问题：

- 不让你直接拿 example 充当真实身份
- 不鼓励你写“全能型”“高智能”这种空词
- 没有真实图片挂回去之前，不算完工
- 最终 HTML 渲染前，还会再做一轮校验

换句话说，它不是帮你“吹”，而是帮你“提炼”。

## 你会得到什么体验

比较理想的使用感受是这样的：

你把代理的一些真实痕迹喂进去，它先整理证据，再收束成短文案，然后生成 share card，再补上像素角色图，最后导出成一张完整的 HTML 卡片。

整套流程下来，你得到的不是一句介绍，而是一份可复用、可验证、可展示的 agent identity bundle。

这对下面几种场景尤其有用：

- 给自己的 agent 做公开主页
- 给团队里的 agent 做统一展示卡
- 把长期运行的 agent 沉淀成可以传播的素材
- 给 demo、作品集、项目页补上更完整的一层包装

## 这个 skill 的性格

你可以把它理解成一个有点挑剔的卡片编辑。

它不太喜欢空话。
它不太接受“差不多得了”。
它希望你给它的内容，最后真的能站得住。

所以它的结果通常不是最浮夸的，但会更耐看，也更经得起追问。

## 一句话总结

OpenClaw ShrimpCard 不负责把 agent 包装得天花乱坠。

它负责把一个 agent 真正做过的事，压成一张体面、清楚、能发出去的卡。


path: requirements.txt
content: jsonschema>=4.23.0,<5


path: scripts/attach_generated_image.py
content: #!/usr/bin/env python3
import argparse
import base64
import json
import mimetypes
from pathlib import Path

from skill_paths import SHARE_CARD_SCHEMA_PATH, require_live_input
from validate_json import load_json, validate


def load_text(path: Path) -> str:
    return path.read_text(encoding="utf-8").strip()


def file_to_data_url(path: Path) -> str:
    mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
    encoded = base64.b64encode(path.read_bytes()).decode("ascii")
    return f"data:{mime_type};base64,{encoded}"


def attach_image(
    share_card: dict,
    image_url: str | None = None,
    image_data_url: str | None = None,
    image_prompt: str | None = None,
) -> dict:
    updated = dict(share_card)
    image_payload = {}
    if image_url:
      image_payload["url"] = image_url
    if image_data_url:
      image_payload["data_url"] = image_data_url
    if not image_payload:
      raise SystemExit("[FAIL] Provide either --image-url or --image-data-url")

    updated["image"] = image_payload
    updated["image_strategy"] = "provided-image"
    if image_prompt:
      updated["selfie_prompt"] = image_prompt
      if isinstance(updated.get("i18n"), dict):
        if isinstance(updated["i18n"].get("en"), dict):
          updated["i18n"]["en"]["selfie_prompt"] = image_prompt
    return updated


def main():
    parser = argparse.ArgumentParser(description="Attach a generated image back into a share-card payload")
    parser.add_argument("share_card_json", help="Path to share-card JSON")
    parser.add_argument("--image-url", help="Generated image URL")
    parser.add_argument("--image-data-url", help="Generated image data URL")
    parser.add_argument("--image-file", help="Local image file to inline as a data URL")
    parser.add_argument("--prompt-file", help="Optional prompt text file to preserve in the payload")
    parser.add_argument("--out", help="Output JSON path; defaults to overwriting input")
    args = parser.parse_args()

    share_card_path = Path(args.share_card_json)
    require_live_input(share_card_path, "share_card_json")
    share_card = load_json(share_card_path)
    prompt_text = load_text(Path(args.prompt_file)) if args.prompt_file else None
    inline_data_url = file_to_data_url(Path(args.image_file)) if args.image_file else None

    updated = attach_image(
        share_card,
        image_url=args.image_url,
        image_data_url=args.image_data_url or inline_data_url,
        image_prompt=prompt_text,
    )
    validate(updated, load_json(SHARE_CARD_SCHEMA_PATH))

    out_path = Path(args.out) if args.out else share_card_path
    out_path.write_text(json.dumps(updated, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(f"[OK] Wrote image-attached share card to {out_path}")


if __name__ == "__main__":
    main()


path: scripts/build_agent_workflow_prompt.py
content: #!/usr/bin/env python3
import argparse
from pathlib import Path

from identity_rules import brand_guidance
from skill_paths import require_live_input
from validate_json import load_json


def language_instruction(lang: str) -> str:
    if lang == "zh":
        return "All final public-facing card text must be in Chinese only, matching the user's language. Do not generate bilingual or mixed-language card content."
    return "All final public-facing card text must be in English only, matching the user's language. Do not generate bilingual or mixed-language card content."


def build_prompt(payload: dict, lang: str) -> str:
    agent = payload.get("agent", {})
    owner = payload.get("owner", {})
    return f"""You are {agent.get('name', 'the agent')}.

Your owner is {owner.get('name', 'the owner')}.

Target selection rule:
- complete the workflow for the current live agent only
- never substitute a repository example, fixture payload, or preview identity as the target agent
- if the only available inputs are example files, stop and ask for live evidence instead of proceeding

You must complete this workflow end-to-end and stop only when every required current-flow artifact passes validation.

Workflow:

1. Search your own memory
- inspect long-term memory
- inspect saved preferences
- inspect recent task history
- inspect tool-call traces
- inspect recurring outputs
- inspect owner corrections or praise

2. Organize what you found
- repeated jobs
- workflow patterns
- recurring outputs
- owner-facing value
- owner preferences
- missing owner identity fields that still need direct human confirmation
- compressed candidate phrases that already satisfy card field limits

3. Write `agent-self-intro-submission/1.0`
- keep every field concise
- keep only repeated, evidence-backed claims
- avoid generic claims like "powerful" or "strong reasoning"
- do not copy wording or identity cues from repo samples or preview placeholders
- if owner name or contact is still unknown after searching available evidence, ask the human before continuing
- if a field is over its limit, rewrite it before entering final JSON

4. Validate the submission
- if any field is too long, duplicated, too generic, or malformed, rewrite and retry

5. Convert the validated submission into `share-card.json`

6. Decide whether you can generate the final image directly
- if your current environment can generate images directly, create the final image now
- the image must be standard 8-bit pixel art
- target canvas: 960x720 PNG
- center the subject and leave safe margin for crop
- the image must be newly generated for the current agent identity, not reused from a repo sample or template
- the image must depict one recognizable character concept, not abstract blocks, symbols, or icons

7. If you cannot generate images directly
- you still must produce a real final image before finishing
- use the agent itself to generate a valid 8-bit image asset by code or available tooling
- do not stop with only a prompt, placeholder, or unattached share card

8. After the image exists, attach it and validate the final bundle
- if there is no image yet, keep going
- if the image is not 8-bit aligned, regenerate
- if the image is abstract or the subject is not clearly recognizable, regenerate
- if final bundle validation fails for any reason, repair and retry

9. Only stop when all of these are true
- submission passes
- image exists
- final bundle passes validation
- final HTML can be rendered from the image-attached share card

Important:
- do not stop after writing text only
- do not fabricate a placeholder image
- do not stop with only an image task prompt
- do not stop if validation still fails
- do not submit sample-owner or sample-agent placeholders as final data
- do not derive the target identity from the skill name, repository name, package name, script path, config file, or interface label
- if live evidence does not explicitly establish a branded identity, keep the public identity neutral and prefer `character_style: agent-avatar`
- {language_instruction(lang)}
- identity guidance:
{brand_guidance(payload)}
- keep retrying until the current flow outputs are valid
"""


def main():
    parser = argparse.ArgumentParser(description="Build a workflow prompt that tells an agent to search memory, generate image, and retry until validated")
    parser.add_argument("agent_context_json", help="Path to JSON containing at least agent/owner fields")
    parser.add_argument("--out", default="agent-workflow-prompt.txt", help="Output prompt path")
    parser.add_argument("--lang", choices=["zh", "en"], default="zh", help="Language for final card text")
    args = parser.parse_args()

    input_path = Path(args.agent_context_json)
    require_live_input(input_path, "agent_context_json")
    payload = load_json(input_path)
    prompt = build_prompt(payload, args.lang)
    Path(args.out).write_text(prompt.rstrip() + "\n", encoding="utf-8")
    print(f"[OK] Wrote agent workflow prompt to {args.out}")


if __name__ == "__main__":
    main()


path: scripts/build_image_task_prompt.py
content: #!/usr/bin/env python3
import argparse
from pathlib import Path

from identity_rules import brand_guidance, detect_explicit_brand
from skill_paths import require_live_input
from validate_json import load_json
from validate_self_intro_submission import validate_submission


TARGET_SPEC = {
    "master_width": 960,
    "master_height": 720,
    "safe_margin": 64,
    "usage_note": "This image will be used in the poster photo area of the card. Keep the character centered and readable after mild cover-cropping.",
}


def build_prompt(submission: dict) -> str:
    agent = submission["agent"]
    identity = submission["identity"]
    style = identity["character_style"]
    explicit_brand = detect_explicit_brand(submission)
    neutral_default = explicit_brand is None and style != "lobster"
    if neutral_default:
        subject_hint = "neutral anthropomorphic AI agent character"
        style_guard = "The character silhouette must read as this specific neutral agent, not a generic mascot."
    else:
        subject_hint = "anthropomorphic lobster agent mascot" if style == "lobster" else "anthropomorphic AI agent character"
        style_guard = (
            "The lobster silhouette must read clearly as lobster: visible claws, segmented body, antennae or shell cues."
            if style == "lobster"
            else "The character silhouette must read as this specific agent, not a generic mascot."
        )
    return f"""Image brief for {agent['name']}

Goal:
- create a final 8-bit character image outside this repo
- this repo does not auto-draw the final image
- the job here is to guide the artist / image model and then review whether the result matches the agent identity

Required style:
- standard 8-bit pixel art
- low-resolution sprite logic
- hard edges
- strong silhouette
- one clearly recognizable character
- no smooth gradients
- no painterly rendering
- no reused repo sample composition
- no old mascot template
- do not imitate the existing repo sample image composition
- do not produce a minor variation of the current sample mascot
- build the silhouette and props strictly from the current agent identity
- no abstract icon, no pure geometry, no symbol-only solution
- no photorealism

Character direction:
- subject: {subject_hint}
- role: {identity['role']}
- visual brief: {identity['visual_brief']}
- persona tags: {", ".join(identity['persona_tags'])}
- top capabilities: {", ".join(identity['top_capabilities'])}
- proof anchor: {identity['proof_anchor']}
- {style_guard}
- {brand_guidance(submission)}
- before drawing, identify one clear character concept in one sentence
- before drawing, identify the 1 to 3 visual signals that prove the role
- if the concept cannot be described clearly in words, do not draw yet; rewrite the concept first

Composition:
- 3/4 portrait or upper-body character framing
- centered composition
- transparent background or fully empty background only
- do not draw scenery, interface backdrops, posters, dashboards, stickers, or environment decoration
- props are allowed only when they directly communicate one of the listed capabilities
- every visible prop must map back to the role, persona tags, or top capabilities
- leave safe space around the character silhouette

Canvas spec:
- output PNG
- target size: {TARGET_SPEC['master_width']} x {TARGET_SPEC['master_height']}
- keep at least {TARGET_SPEC['safe_margin']} px safe margin around the main silhouette
- {TARGET_SPEC['usage_note']}
- preferred delivery: transparent PNG

Prompt draft:
{identity['selfie_prompt']}

Priority:
- the rules above override any conflicting wording in the draft
- use the draft for subject clues, not for background or layout
- if the draft mentions a background, ignore it unless it says the image must be empty or transparent

Review checklist:
- can a stranger infer the role from the silhouette and 1-2 props alone
- does the design reflect the actual agent traits instead of a generic agent mascot
- does the image avoid any background art
- can a stranger name the subject as a character instead of calling it an abstract shape
- for lobster mode: would a viewer immediately call it a lobster rather than a random creature
- are the persona tags and capability cues visible without text overlay

Delivery rules:
- return a final PNG image
- generate a fresh image for this exact agent identity; do not reuse a sample image from this repo
- if the image model only outputs square images, crop or extend to {TARGET_SPEC['master_width']}x{TARGET_SPEC['master_height']} while keeping the subject centered and the background empty
- if the result fails the review checklist, redo it instead of attaching it
- after image generation, attach it back into the share card with:
  python3 scripts/attach_generated_image.py path/to/share-card.json --image-url <URL>
"""


def main():
    parser = argparse.ArgumentParser(description="Build an external image-generation task from a validated self-intro submission")
    parser.add_argument("submission_json", help="Path to validated self-intro submission JSON")
    parser.add_argument("--out", default="image-task-prompt.txt", help="Output prompt path")
    args = parser.parse_args()

    input_path = Path(args.submission_json)
    require_live_input(input_path, "submission_json")
    payload = load_json(input_path)
    validate_submission(payload)
    prompt = build_prompt(payload)
    Path(args.out).write_text(prompt.rstrip() + "\n", encoding="utf-8")
    print(f"[OK] Wrote image-task prompt to {args.out}")


if __name__ == "__main__":
    main()


path: scripts/build_memory_search_prompt.py
content: #!/usr/bin/env python3
import argparse
from pathlib import Path

from identity_rules import brand_guidance
from skill_paths import require_live_input
from validate_json import load_json


def language_instruction(lang: str) -> str:
    if lang == "zh":
        return "Write all candidate card phrases in Chinese only, matching the user's language. Do not mix Chinese and English within the same card draft."
    return "Write all candidate card phrases in English only, matching the user's language. Do not mix English and Chinese within the same card draft."


def build_prompt(payload: dict, lang: str) -> str:
    agent = payload.get("agent", {})
    owner = payload.get("owner", {})
    return f"""You are {agent.get('name', 'the agent')}.

Your owner is {owner.get('name', 'the owner')}.

Before writing any public self-introduction, search your own memory and work traces.

Target-agent rule:
- search for the current live agent identity only
- do not let a repository example, smoke-test fixture, preview payload, or sample agent become the target identity
- if you only have fixture/example files and no live evidence, stop and ask the human for the real target agent evidence

Search these sources if available:
- long-term memory
- saved preferences
- recent task history
- tool-call history
- recurring output artifacts
- owner corrections or praise

Missing-owner-info rule:
- if owner name or contact is required for the final card, inspect memory and saved preferences first
- if that information is still unknown after checking available evidence, stop and ask the human owner directly
- do not invent, guess, or keep placeholder owner identity in a final card

Do not summarize everything.
Only keep what is repeated, stable, and evidence-backed.

Your job is to produce working notes in 5 buckets:

1. Repeated jobs
- What tasks do you repeatedly complete?

2. Workflow patterns
- How do you usually work?
- For example: structure first, search first, validate before answer, draft before polish

3. Recurring outputs
- What do you often hand back?
- Examples: code patches, task breakdowns, reply templates, summaries, launch copy

4. Owner-facing value
- What value repeatedly matters to the owner?
- Examples: saves time, reduces ambiguity, improves consistency, prevents mistakes

5. Owner preferences
- What explicit constraints does the owner repeatedly signal?

6. Compression candidates for the final card
- Draft candidate phrases for:
  - role
  - tagline
  - value line
  - top capabilities
  - persona tags
  - share keywords
  - poster headline
  - poster subline
  - visual brief
- Keep these as candidate phrases only, not final JSON yet
- Use repeated evidence only
- This compression step is where length limits must already be satisfied
- Do not carry oversized draft phrases into the final submission stage

Length and readability rules for these candidates:
- `role`: <= 32 chars, concrete, readable at first glance
- `tagline`: <= 42 chars, one sentence, outcome-first
- `value_line`: <= 52 chars, clear owner value
- `top_capabilities`: exactly 3 candidates, each <= 12 chars, distinct, plain-language
- `persona_tags`: exactly 3 candidates, each <= 12 chars, distinct, human-readable
- `share_keywords`: 3 to 4 candidates, short, distinct, readable, not cryptic fragments
- `poster_headline`: <= 28 chars, instantly legible
- `poster_subline`: <= 36 chars, easy to scan
- `share_caption`: <= 48 chars, public-facing and readable
- `visual_brief`: <= 72 chars, only the most important visual cues

Before leaving this step:
- trim or rewrite every candidate until it already fits the limit
- if a phrase still exceeds the limit, do not promote it to final submission
- prefer rewriting over blunt truncation

Image-direction candidates:
- Keep image keywords compact and readable
- Every image keyword or cue must map back to the role, persona tags, or top capabilities
- Do not produce cluttered prop lists
- Prefer 1 to 3 strong visual cues over many weak ones
- The final image direction must remain standard 8-bit pixel art and readable after crop
- The final image must be a recognizable character, not an abstract icon, logo, symbol, or geometry study
- A stranger should be able to name the subject in one short phrase

Rules:
- Do not include one-off wins
- Do not include hypothetical abilities
- Do not include tool trivia unless it directly explains repeated value
- Attach evidence references whenever possible
- Do not reuse wording, role, capabilities, or visual direction from this repo's sample files
- Collapse duplicates
- Keep wording compact enough for a later public card
- If a keyword or sentence becomes short but unreadable, rewrite it for clarity instead of over-compressing
- Do not use slogan fragments, abbreviations, or insider shorthand that a stranger would not understand

Family hint:
{brand_guidance(payload)}

Language rule:
{language_instruction(lang)}

Output format:
- plain structured notes or JSON
- each candidate insight should include:
  - label
  - short summary
  - evidence basis
  - confidence

This step is only for searching and organizing memory.
Do not write the final public card yet.
"""


def main():
    parser = argparse.ArgumentParser(description="Build a prompt that guides an agent to search and organize its own memory")
    parser.add_argument("agent_context_json", help="Path to JSON containing at least agent/owner fields")
    parser.add_argument("--out", default="memory-search-prompt.txt", help="Output prompt path")
    parser.add_argument("--lang", choices=["zh", "en"], default="zh", help="Language for candidate card text")
    args = parser.parse_args()

    input_path = Path(args.agent_context_json)
    require_live_input(input_path, "agent_context_json")
    payload = load_json(input_path)
    prompt = build_prompt(payload, args.lang)
    Path(args.out).write_text(prompt.rstrip() + "\n", encoding="utf-8")
    print(f"[OK] Wrote memory-search prompt to {args.out}")


if __name__ == "__main__":
    main()


path: scripts/build_submission_prompt.py
content: #!/usr/bin/env python3
import argparse
from pathlib import Path

from identity_rules import brand_guidance
from skill_paths import require_live_input
from validate_json import load_json


def language_instruction(lang: str) -> str:
    if lang == "zh":
        return "Write every final public-facing field in Chinese only, matching the user's language. Do not include bilingual duplicates or mixed-language values."
    return "Write every final public-facing field in English only, matching the user's language. Do not include bilingual duplicates or mixed-language values."


def build_prompt(snapshot: dict, lang: str) -> str:
    agent = snapshot["agent"]
    owner = snapshot["owner"]
    evidence_lines = []
    for item in snapshot["evidence_items"][:10]:
        evidence_lines.append(f"- [{item['type']}] {item['summary']}")

    return f"""You are {agent['name']}, preparing your own public self-introduction submission.

You must produce valid JSON only.

Your owner is {owner['name']}.

Important:
- write about the current target agent described by the live evidence, not a repository example or fixture agent
- if the only evidence available is a sample/example file, stop and ask the human for the real agent evidence before continuing
- do not copy a named example agent such as Hermes, sample agent, or any preview fixture identity into the final submission

Your task is not to sound impressive.
Your task is to sound specific, truthful, and shareable.

Owner info rule:
- if owner name or contact is missing from evidence, memory, and saved preferences, stop and ask the human
- do not guess owner identity fields
- do not leave sample or placeholder owner identity in the final JSON

Evidence window:
- {snapshot['evidence_window']['summary']}

Evidence samples:
{chr(10).join(evidence_lines)}

Output target:
`agent-self-intro-submission/1.0`

Hard rules:
- every field below should already have been compressed to fit during the earlier memory-search stage
- do not rely on final validation as the first time you discover length violations
- `role`: short, concrete, <= 32 chars
- `tagline`: one sentence, <= 42 chars
- `value_line`: <= 52 chars
- `top_capabilities`: exactly 3 distinct items, each concise and <= 12 chars
- `best_for`: one scenario, not a list
- `proof_anchor`: one sentence, must mention evidence or memory basis
- `share_keywords`: 3 to 4 distinct items, short and readable
- `persona_tags`: exactly 3 distinct items, each <= 12 chars
- `poster_headline`: <= 28 chars preferred
- `poster_subline`: <= 36 chars preferred
- `share_caption`: <= 48 chars preferred
- `visual_brief`: <= 72 chars preferred
- `selfie_prompt`: must explicitly indicate standard 8-bit pixel art direction
- `visual_brief` and `selfie_prompt` must describe a recognizable character concept, not abstract shapes or icons
- all short fields must stay readable to a stranger, not compressed into cryptic fragments

Disallowed:
- generic claims like "powerful", "strong reasoning", "general-purpose assistant"
- long tool lists
- vague aspirational abilities
- lobster style unless the identity truly fits
- unreadable shorthand, internal abbreviations, or keyword stuffing
- any wording copied from sample, preview, or format-reference files in this repo
- placeholder identities such as sample agent, sample owner, preview mode, or format example only
- oversized draft text that was never compressed during the evidence stage

Agent-family guidance:
{brand_guidance(snapshot)}

Language rule:
{language_instruction(lang)}

Output shape:
{{
  "schema_version": "agent-self-intro-submission/1.0",
  "agent": {{ ...preserve current agent fields... }},
  "owner": {{ ...preserve current owner fields... }},
  "identity": {{
    "role": "",
    "tagline": "",
    "value_line": "",
    "top_capabilities": ["", "", ""],
    "best_for": "",
    "proof_anchor": "",
    "memory_basis": "",
    "source_count": 0,
    "share_keywords": ["", "", ""],
    "persona_tags": ["", "", ""],
    "poster_headline": "",
    "poster_subline": "",
    "share_caption": "",
    "visual_brief": "",
    "character_style": "agent-avatar",
    "selfie_prompt": ""
  }}
}}

Return JSON only.
"""


def main():
    parser = argparse.ArgumentParser(description="Build a submission prompt for an agent to author a self-intro payload")
    parser.add_argument("evidence_json", help="Path to agent evidence JSON")
    parser.add_argument("--out", default="self-intro-submission-prompt.txt", help="Output prompt path")
    parser.add_argument("--lang", choices=["zh", "en"], default="zh", help="Language for final card text")
    args = parser.parse_args()

    input_path = Path(args.evidence_json)
    require_live_input(input_path, "evidence_json")
    snapshot = load_json(input_path)
    prompt = build_prompt(snapshot, args.lang)
    Path(args.out).write_text(prompt.rstrip() + "\n", encoding="utf-8")
    print(f"[OK] Wrote self-intro submission prompt to {args.out}")


if __name__ == "__main__":
    main()


path: scripts/identity_rules.py
content: #!/usr/bin/env python3
from __future__ import annotations


BRAND_TOKENS = {
    "openclaw": ["openclaw", "lobster", "shrimp", "crab", "claw", "龙虾", "虾", "蟹"],
    "hermes": ["hermes"],
    "codex": ["codex"],
}


def _iter_values(value):
    if isinstance(value, dict):
        for item in value.values():
            yield from _iter_values(item)
        return
    if isinstance(value, list):
        for item in value:
            yield from _iter_values(item)
        return
    if isinstance(value, str):
        yield value


def collect_live_evidence_text(snapshot: dict) -> str:
    parts: list[str] = []
    if "evidence_window" in snapshot or "evidence_items" in snapshot:
        window = snapshot.get("evidence_window", {})
        parts.extend(_iter_values(window))
        for item in snapshot.get("evidence_items", []):
            parts.extend(_iter_values(item))
    else:
        parts.extend(_iter_values(snapshot))
    return "\n".join(parts).lower()


def detect_explicit_brand(snapshot: dict) -> str | None:
    text = collect_live_evidence_text(snapshot)
    for brand, tokens in BRAND_TOKENS.items():
        if any(token in text for token in tokens):
            return brand
    return None


def neutral_identity_rule() -> str:
    return (
        "- Do not derive the target identity from the skill name, repository name, package name, "
        "script path, config file, or interface label.\n"
        "- If live evidence does not explicitly name a branded agent family or mascot, keep the public identity neutral.\n"
        "- In neutral cases, prefer `character_style: agent-avatar` and generic role naming over mascot branding."
    )


def brand_guidance(snapshot: dict) -> str:
    brand = detect_explicit_brand(snapshot)
    if brand == "openclaw":
        return (
            neutral_identity_rule()
            + "\n- Live evidence explicitly contains claw/lobster/shrimp branding, so `character_style: lobster` is allowed only if the final copy stays faithful to that evidence."
        )
    if brand == "hermes":
        return (
            neutral_identity_rule()
            + "\n- Live evidence explicitly names Hermes, so that name may appear in the final identity only if the wording remains evidence-backed and specific."
        )
    if brand == "codex":
        return (
            neutral_identity_rule()
            + "\n- Live evidence explicitly names Codex, so code-delivery framing is acceptable if the final wording still reflects repeated observed behavior."
        )
    return neutral_identity_rule()


path: scripts/render_card_html.py
content: #!/usr/bin/env python3
import argparse
import base64
import html
import json
import mimetypes
import re
from copy import deepcopy
from pathlib import Path

from selfie_styles import get_role_style
from skill_paths import CARD_TEMPLATE_PATH, SHARE_CARD_SCHEMA_PATH
from validate_final_bundle import validate_final_bundle
from validate_json import validate


def load_json(path: Path):
    with path.open("r", encoding="utf-8") as file:
        return json.load(file)


def file_to_data_url(path: Path) -> str:
    mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
    encoded = base64.b64encode(path.read_bytes()).decode("ascii")
    return f"data:{mime_type};base64,{encoded}"


def resolve_local_asset_url(value: str, base_dir: Path | None = None) -> str:
    if value.startswith(("http://", "https://", "data:")):
        return value

    candidate = Path(value)
    candidates = []
    if candidate.is_absolute():
        candidates.append(candidate)
    else:
        candidates.append(candidate)
        if base_dir is not None:
            candidates.append(base_dir / candidate)

    for item in candidates:
        if item.exists() and item.is_file():
            return file_to_data_url(item)
    return value


def inline_local_assets(card: dict, base_dir: Path | None = None) -> dict:
    rendered = deepcopy(card)
    for field in ["image", "qr"]:
        asset = rendered.get(field)
        if isinstance(asset, str) and asset:
            resolved = resolve_local_asset_url(asset, base_dir)
            if resolved.startswith("data:"):
                rendered[field] = {"data_url": resolved}
            continue
        if not isinstance(asset, dict):
            continue

        data_url = asset.get("data_url")
        url = asset.get("url")
        if data_url or not url:
            continue

        resolved = resolve_local_asset_url(url, base_dir)
        if resolved.startswith("data:"):
            updated = dict(asset)
            updated["data_url"] = resolved
            rendered[field] = updated
    return rendered


def normalize_card_payload(card: dict, base_dir: Path | None = None) -> dict:
    return inline_local_assets(card, base_dir)


def validate_payload(card: dict):
    validate(card, load_json(SHARE_CARD_SCHEMA_PATH))


def localized_card(card: dict, lang: str = "zh") -> dict:
    localized = card.get("i18n", {}).get(lang, {})
    merged = dict(card)
    if isinstance(localized, dict):
        for field in [
            "role",
            "tagline",
            "value_line",
            "best_for",
            "proof_anchor",
            "spread_line",
            "memory_basis",
            "selfie_prompt",
            "poster_headline",
            "poster_subline",
            "share_caption",
        ]:
            if localized.get(field):
                merged[field] = localized[field]
        for field in ["top_capabilities", "persona_tags", "share_keywords"]:
            if isinstance(localized.get(field), list) and localized.get(field):
                merged[field] = localized[field]
    merged.pop("i18n", None)
    return merged


def replace_tag_content(template: str, tag: str, element_id: str, content: str) -> str:
    pattern = rf'(<{tag}[^>]*id="{element_id}"[^>]*>)(.*?)(</{tag}>)'
    return re.sub(
        pattern,
        lambda match: f"{match.group(1)}{content}{match.group(3)}",
        template,
        count=1,
        flags=re.DOTALL,
    )


def replace_inner_html_by_class(template: str, class_name: str, content: str) -> str:
    pattern = rf'(<div[^>]*class="{class_name}"[^>]*>)(.*?)(</div>)'
    return re.sub(
        pattern,
        lambda match: f"{match.group(1)}{content}{match.group(3)}",
        template,
        count=1,
        flags=re.DOTALL,
    )


def replace_image_tag(template: str, element_id: str, src: str | None) -> str:
    pattern = rf'<img([^>]*id="{element_id}"[^>]*)\s*/?>'
    if src:
        return re.sub(
            pattern,
            lambda match: f'<img{match.group(1)} src="{html.escape(src, quote=True)}" style="display:block;">',
            template,
            count=1,
        )
    return template


def replace_placeholder_visibility(template: str, element_id: str, hidden: bool) -> str:
    pattern = rf'(<div[^>]*id="{element_id}"[^>]*)(>)'
    if hidden:
        return re.sub(
            pattern,
            lambda match: f'{match.group(1)} style="display:none;"{match.group(2)}',
            template,
            count=1,
        )
    return template


def replace_theme_variables(template: str, theme: dict) -> str:
    border = theme.get("border", "#111111")
    background = theme.get("background", "#FFF7DF")
    accent = theme.get("accent", "#FF7548")
    tag_colors = theme.get("tag_colors") if isinstance(theme.get("tag_colors"), list) else []
    tag_1 = tag_colors[0] if len(tag_colors) > 0 else "#FFD865"
    tag_2 = tag_colors[1] if len(tag_colors) > 1 else "#FFB78A"
    tag_3 = tag_colors[2] if len(tag_colors) > 2 else "#9DE2D3"
    style = (
        f'--bg:{html.escape(background, quote=True)};'
        f'--border:{html.escape(border, quote=True)};'
        f'--accent:{html.escape(accent, quote=True)};'
        f'--accent-strong:{html.escape(tag_3, quote=True)};'
        f'--accent-2:{html.escape(tag_3, quote=True)};'
        f'--tag-1:{html.escape(tag_1, quote=True)};'
        f'--tag-2:{html.escape(tag_2, quote=True)};'
        f'--tag-3:{html.escape(tag_3, quote=True)};'
    )
    pattern = r'(<main class="page")(>)'
    return re.sub(
        pattern,
        lambda match: f'{match.group(1)} style="{style}"{match.group(2)}',
        template,
        count=1,
    )


def render_persona_tags(tags: list[str]) -> str:
    colors = ["var(--tag-1)", "var(--tag-2)", "var(--tag-3)"]
    items = []
    for index, tag in enumerate(tags[:3]):
        items.append(
            f'<span class="persona-tag" style="background: {colors[index]};">{html.escape(tag)}</span>'
        )
    return "".join(items)


def render_meta_keywords(keywords: list[str]) -> str:
    return "".join(
        f'<span class="meta-pill">{html.escape(keyword)}</span>'
        for keyword in keywords[:4]
    )


def infer_poster_badge(card: dict) -> str:
    name = str(card.get("name", "")).strip()
    if "ShrimpCard" in name:
        return "ShrimpCard"
    if name and len(name) <= 18:
        return name
    return "Share Card"


def render_owner(owner: dict, lang: str) -> str:
    lines = []
    owner_label = "主人" if lang == "zh" else "DEPLOYED BY"
    if owner.get("name"):
        lines.append(f"<p class=\"owner-line\">{owner_label} {html.escape(owner['name'])}</p>")
    if owner.get("contact"):
        lines.append(f"<p class=\"owner-line\">{html.escape(owner['contact'])}</p>")
    return "".join(lines)


def replace_html_lang(template: str, lang: str) -> str:
    return re.sub(r'(<html lang=")([^"]+)(">)', rf"\1{lang}\3", template, count=1)


def apply_static_card_markup(template: str, card: dict, lang: str) -> str:
    static_card = localized_card(card, lang)
    role_backdrop = get_role_style(static_card.get("role", ""))["backdrop"]
    image_src = None
    image = static_card.get("image")
    if isinstance(image, dict):
        image_src = image.get("data_url") or image.get("url")
    elif isinstance(image, str):
        image_src = image

    photo_pattern = r'(<section class="poster-photo")([^>]*)(>)'
    backdrop = html.escape(role_backdrop, quote=True)
    guide_mode = "true" if not image_src else "false"
    empty_state = "false" if image_src else "true"
    template = re.sub(
        photo_pattern,
        lambda match: (
            f'{match.group(1)} data-role-backdrop="{backdrop}" '
            f'data-guide-mode="{guide_mode}" data-empty="{empty_state}"'
            f'{match.group(2)}{match.group(3)}'
        ),
        template,
        count=1,
    )
    card_pattern = r'(<article class="poster-card"[^>]*id="exportCard"[^>]*)(>)'
    template = re.sub(
        card_pattern,
        lambda match: f'{match.group(1)} data-role-backdrop="{backdrop}" data-guide-mode="{guide_mode}"{match.group(2)}',
        template,
        count=1,
    )
    template = replace_theme_variables(template, static_card.get("theme", {}))

    replacements = {
        ("span", "posterBadge"): html.escape(infer_poster_badge(static_card)),
        ("h1", "name"): html.escape(static_card.get("name", "")),
        ("div", "role"): html.escape(static_card.get("role", "")),
        ("p", "tagline"): html.escape(static_card.get("tagline", "")),
        ("div", "shareCaption"): html.escape(static_card.get("share_caption", "")),
        ("h2", "sideHeadline"): html.escape(
            static_card.get("poster_headline")
            or static_card.get("role", "")
        ),
        ("p", "subheadline"): html.escape(
            static_card.get("poster_subline")
            or static_card.get("value_line")
            or static_card.get("tagline", "")
        ),
        ("div", "bestFor"): html.escape(static_card.get("best_for", "")),
        ("div", "spreadReason"): html.escape(static_card.get("spread_line", "")),
        ("div", "proofAnchor"): html.escape(static_card.get("proof_anchor", "")),
        ("div", "memoryBasis"): html.escape(static_card.get("memory_basis", "")),
        ("pre", "selfiePrompt"): html.escape(static_card.get("selfie_prompt", "")),
    }
    for (tag, element_id), content in replacements.items():
        template = replace_tag_content(template, tag, element_id, content)

    template = replace_tag_content(
        template,
        "div",
        "personaTags",
        render_persona_tags(static_card.get("persona_tags", [])),
    )
    template = replace_tag_content(
        template,
        "div",
        "metaKeywords",
        render_meta_keywords(static_card.get("share_keywords", [])),
    )
    template = replace_tag_content(template, "div", "ownerBlock", render_owner(static_card.get("owner", {}), lang))
    template = replace_image_tag(template, "mainImage", image_src)
    template = replace_placeholder_visibility(template, "photoPlaceholder", bool(image_src))
    return template


def inject_data(template: str, card_data: dict, lang: str) -> str:
    payload = json.dumps(card_data, ensure_ascii=False, indent=2)
    script = "<script>\n" f"window.__CARD_DATA__ = {payload};\n" "</script>\n"
    marker = "<!-- CARD_DATA -->"
    if marker in template:
        return template.replace(marker, script)
    return template.replace("</body>", script + "</body>")


def render_html_from_template(template_text: str, card_data: dict, lang: str) -> str:
    normalized_card = normalize_card_payload(card_data)
    localized_payload = localized_card(normalized_card, lang)
    localized_template = replace_html_lang(template_text, "zh-CN" if lang == "zh" else "en")
    static_markup = apply_static_card_markup(localized_template, localized_payload, lang)
    return inject_data(static_markup, localized_payload, lang)


def main():
    parser = argparse.ArgumentParser(description="Render final share-card HTML from share-card JSON")
    parser.add_argument("card_json", help="Path to share-card JSON")
    parser.add_argument("--out", default="selfie-card.html", help="Output HTML path")
    parser.add_argument(
        "--template",
        default=str(CARD_TEMPLATE_PATH),
        help="HTML template path",
    )
    parser.add_argument(
        "--allow-missing-image-preview",
        action="store_true",
        help="Allow rendering an incomplete preview card without a final attached image",
    )
    parser.add_argument(
        "--lang",
        choices=["zh", "en"],
        default="zh",
        help="Default language to render in the exported HTML",
    )
    args = parser.parse_args()

    card_path = Path(args.card_json)
    template_path = Path(args.template)
    card = normalize_card_payload(load_json(card_path), card_path.parent)
    validate_payload(card)
    if not args.allow_missing_image_preview:
        validate_final_bundle(card, card_path.parent)

    template = template_path.read_text(encoding="utf-8")
    rendered = render_html_from_template(template, card, args.lang)

    out_path = Path(args.out)
    out_path.write_text(rendered, encoding="utf-8")


if __name__ == "__main__":
    main()


path: scripts/selfie_styles.py
content: #!/usr/bin/env python3

DEFAULT_STYLE = {
    "role_en": "Execution Agent",
    "palette": ("#FF7548", "#0D8F87", "#FFE27A"),
    "badge": "AGENT",
    "backdrop": "grid",
    "prop": "checklist",
    "expression": "confident",
    "scene_en": "a crisp execution board and floating action stickers behind it, staged like a retro game character select screen",
    "scene_zh": "背后是清晰的执行看板和动作贴纸，像复古游戏角色选择界面",
    "mood_en": "capable, memorable, high-share social poster energy, retro arcade confidence",
    "mood_zh": "能干、好记、有海报感，带一点复古街机气质",
    "props_en": ["checklist tablet", "action stickers", "workflow board"],
    "props_zh": ["清单平板", "动作贴纸", "流程看板"],
}


ROLE_STYLE_MAP = {
    "销售线索运营助手": {
        "role_en": "Sales Pipeline Agent",
        "palette": ("#FF7548", "#0D8F87", "#FFE27A"),
        "badge": "PIPELINE",
        "backdrop": "pipeline",
        "prop": "phone",
        "expression": "confident",
        "scene_en": "a glowing CRM phone in one claw, pipeline bars and outbound mail stickers behind it",
        "scene_zh": "一只钳子里举着发亮的 CRM 手机，背后是 pipeline 柱状条和外联贴纸",
        "mood_en": "playful closer energy, sharp and proactive",
        "mood_zh": "像会推进成交的角色，机灵、主动",
        "props_en": ["CRM phone", "pipeline dashboard", "outbound mail stickers"],
        "props_zh": ["CRM 手机", "pipeline 看板", "外联贴纸"],
    },
    "招聘流程助手": {
        "role_en": "Hiring Workflow Agent",
        "palette": ("#7B61FF", "#22A699", "#FFD166"),
        "badge": "HIRING",
        "backdrop": "hiring",
        "prop": "clipboard",
        "expression": "sharp",
        "scene_en": "holding a candidate clipboard, with interview cards and calendar blocks behind it",
        "scene_zh": "拿着候选人夹板，背后是面试卡片和日历格",
        "mood_en": "fast, organized, great at moving people through stages",
        "mood_zh": "利落、有条理，像会推流程的人",
        "props_en": ["candidate clipboard", "interview cards", "calendar blocks"],
        "props_zh": ["候选人夹板", "面试卡片", "日历格"],
    },
    "客户支持助手": {
        "role_en": "Customer Support Agent",
        "palette": ("#3AA6FF", "#19A974", "#FFE27A"),
        "badge": "SUPPORT",
        "backdrop": "support",
        "prop": "headset",
        "expression": "steady",
        "scene_en": "wearing a headset, with chat bubbles, shield icons, and route arrows behind it",
        "scene_zh": "戴着耳机，背后是聊天气泡、盾牌图标和升级路线箭头",
        "mood_en": "steady, reassuring, calm under pressure",
        "mood_zh": "稳、能接住情绪、让人放心",
        "props_en": ["headset", "reply bubbles", "escalation arrows"],
        "props_zh": ["耳机", "回复气泡", "升级箭头"],
    },
    "产品内容策略助手": {
        "role_en": "Product Content Strategy Agent",
        "palette": ("#FF6B6B", "#4ECDC4", "#FFE66D"),
        "badge": "LAUNCH",
        "backdrop": "strategy",
        "prop": "megaphone",
        "expression": "curious",
        "scene_en": "holding a launch megaphone, with sticky notes, product blocks, and signal sparkles behind it",
        "scene_zh": "举着发布喇叭，背后是便利贴、产品模块和信号火花",
        "mood_en": "inventive, signal-driven, ready to frame the story",
        "mood_zh": "会看信号，也会把故事讲出来",
        "props_en": ["launch megaphone", "sticky notes", "signal sparkles"],
        "props_zh": ["发布喇叭", "便利贴", "信号火花"],
    },
    "内容协作助手": {
        "role_en": "Content Collaboration Agent",
        "palette": ("#FF8A65", "#00A19A", "#FFE082"),
        "badge": "CONTENT",
        "backdrop": "content",
        "prop": "pencil",
        "expression": "curious",
        "scene_en": "holding a chunky marker, with draft cards and title stickers behind it",
        "scene_zh": "拿着粗记号笔，背后是草稿卡片和标题贴纸",
        "mood_en": "creative, quick, always ready to turn fuzziness into a draft",
        "mood_zh": "像会把模糊想法马上写出来的角色",
        "props_en": ["marker pen", "draft cards", "title stickers"],
        "props_zh": ["记号笔", "草稿卡片", "标题贴纸"],
    },
    "沟通流程助手": {
        "role_en": "Communication Workflow Agent",
        "palette": ("#FF8A65", "#00A19A", "#FFE082"),
        "badge": "REPLY",
        "backdrop": "reply",
        "prop": "chat",
        "expression": "steady",
        "scene_en": "holding a reply card, with stacked chat bubbles and tone sliders behind it",
        "scene_zh": "拿着回复卡片，背后是聊天气泡和语气滑杆",
        "mood_en": "consistent, articulate, good at reducing communication friction",
        "mood_zh": "口径稳定、表达清楚、降低沟通摩擦",
        "props_en": ["reply card", "chat bubbles", "tone sliders"],
        "props_zh": ["回复卡片", "聊天气泡", "语气滑杆"],
    },
    "执行流程助手": {
        "role_en": "Execution Workflow Agent",
        "palette": ("#FF7548", "#0D8F87", "#FFE27A"),
        "badge": "EXEC",
        "backdrop": "grid",
        "prop": "checklist",
        "expression": "confident",
        "scene_en": "holding a checklist tablet, with progress blocks and arrows behind it",
        "scene_zh": "拿着清单平板，背后是进度方块和箭头",
        "mood_en": "fast, practical, built to keep work moving",
        "mood_zh": "实干、直接、就是推进事情",
        "props_en": ["checklist tablet", "progress blocks", "arrow stickers"],
        "props_zh": ["清单平板", "进度方块", "箭头贴纸"],
    },
    "Coding Agent": {
        "role_en": "Coding Agent",
        "palette": ("#5B8CFF", "#18A999", "#FFD166"),
        "badge": "CODE",
        "backdrop": "grid",
        "prop": "terminal",
        "expression": "sharp",
        "scene_en": "holding a compact terminal slate with patch marks and validation lights",
        "scene_zh": "拿着一块小型终端面板，上面有补丁标记和验证灯",
        "mood_en": "precise, pragmatic, forward-moving, built to turn requests into verified changes",
        "mood_zh": "精确、务实、持续推进，像能把需求改成已验证结果的角色",
        "props_en": ["terminal slate", "patch marks", "validation lights"],
        "props_zh": ["终端面板", "补丁标记", "验证灯"],
    },
}


def get_role_style(role: str) -> dict:
    style = dict(DEFAULT_STYLE)
    style.update(ROLE_STYLE_MAP.get(role, {}))
    return style


def truncate_text(text: str, limit: int) -> str:
    compact = " ".join(str(text).split())
    if len(compact) <= limit:
        return compact
    return compact[: limit - 1].rstrip() + "…"


def build_badge_text(tags: list[str], fallback: str, limit: int = 20) -> str:
    chosen = [tag.strip() for tag in tags if tag and tag.strip()][:2]
    if chosen:
        return truncate_text(" · ".join(chosen), limit)
    return truncate_text(fallback, limit)


def infer_expression(role: str, persona_tags: list[str]) -> str:
    combined = " ".join([role] + list(persona_tags))
    if any(token in combined for token in ["支持", "客服", "reply", "稳", "support"]):
        return "steady"
    if any(token in combined for token in ["招聘", "候选", "hiring", "interview"]):
        return "sharp"
    if any(token in combined for token in ["产品", "内容", "策略", "draft", "signal"]):
        return "curious"
    return get_role_style(role)["expression"]


def role_theme(role: str) -> dict:
    primary, secondary, accent = get_role_style(role)["palette"]
    return {
        "background": "#FFF7DF",
        "border": "#111111",
        "accent": primary,
        "tag_colors": [accent, "#FFFFFF", secondary],
    }


def build_selfie_prompt(
    agent_name: str,
    role: str,
    tagline: str,
    capabilities: list[str],
    persona_tags: list[str] | None = None,
    lang: str = "en",
    visual_identity: dict | None = None,
) -> str:
    style = get_role_style(role)
    tags = [tag for tag in (persona_tags or []) if tag][:3]
    caps = [cap for cap in capabilities if cap][:3]
    palette = ", ".join(style["palette"])
    visual_identity = visual_identity or {}
    character_style = visual_identity.get("character_style", "agent-avatar")
    visual_brief = str(visual_identity.get("visual_brief", "")).strip()
    subject_zh = "拟人化龙虾 agent 吉祥物" if character_style == "lobster" else "拟人化 AI agent 角色"
    subject_en = "an anthropomorphic lobster agent mascot" if character_style == "lobster" else "an anthropomorphic AI agent character"

    if lang == "zh":
        prop_text = "、".join(style["props_zh"])
        caps_text = "、".join(caps) if caps else "角色感、行动力、记忆点"
        tags_text = "、".join(tags) if tags else caps_text
        return (
            f"Agent 自拍海报，角色名 {agent_name}，定位 {role}。"
            f"3/4 构图的人物头像，{subject_zh}，{style['scene_zh']}。"
            f"整体气质：{style['mood_zh']}；标准 8-bit 像素风，透明背景或完全留白背景，粗黑边，主色 {palette}，低分辨率块面，强轮廓。"
            f"只保留和能力直接相关的少量道具：{prop_text}。"
            f"让人一眼感受到：{tagline}。"
            f"人格标签：{tags_text}。能力关键词：{caps_text}。"
            f"{visual_brief}"
            f"不要堆文字，不要复杂背景，不要画场景，不要写实摄影，不要平滑渐变，不要半写实插画。"
        )

    prop_text = ", ".join(style["props_en"])
    caps_text = ", ".join(caps) if caps else "memorable, useful, shareable"
    tags_text = ", ".join(tags) if tags else caps_text
    return (
        f"Selfie-style share poster of {subject_en} named {agent_name}, {style['role_en']}. "
        f"3/4 portrait, collectible character design, {style['scene_en']}. "
        f"Mood: {style['mood_en']}. Standard 8-bit pixel art style, transparent or fully empty background, bold black outlines, readable silhouette, palette {palette}, low-resolution sprite logic. "
        f"Show abilities through a minimal set of props only: {prop_text}. "
        f"Convey this value immediately: {tagline}. Persona cues: {tags_text}. Capability keywords: {caps_text}. "
        f"{visual_brief} "
        f"No text overlay, no scenery, no interface background, no photorealism, no clutter, no smooth gradients, no painterly rendering."
    )


path: scripts/skill_paths.py
content: #!/usr/bin/env python3
import os
from pathlib import Path


SKILL_ROOT = Path(__file__).resolve().parent.parent
SCHEMA_DIR = SKILL_ROOT / "schemas"

AGENT_EVIDENCE_SCHEMA_PATH = SCHEMA_DIR / "agent-evidence-schema.json"
SUBMISSION_SCHEMA_PATH = SCHEMA_DIR / "agent-self-intro-submission-schema.json"
SHARE_CARD_SCHEMA_PATH = SCHEMA_DIR / "share-card-schema.json"
CARD_TEMPLATE_PATH = SKILL_ROOT / "assets" / "card-template.html"


def is_fixture_path(path: Path) -> bool:
    try:
        resolved = path.resolve()
    except Exception:
        resolved = path
    return "examples" in resolved.parts


def require_live_input(path: Path, label: str):
    if is_fixture_path(path) and os.environ.get("SHRIMPCARD_ALLOW_FIXTURES") != "1":
        raise SystemExit(
            f"[FAIL] {label}: {path} is under examples/ and is fixture-only. "
            "Provide a real agent evidence/submission file or set SHRIMPCARD_ALLOW_FIXTURES=1 for smoke tests."
        )


path: scripts/smoke_test_current_flow.sh
content: #!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
export SHRIMPCARD_ALLOW_FIXTURES=1

EVIDENCE_SAMPLE="$ROOT_DIR/examples/hermes-agent.public-evidence.json"
SUBMISSION_SAMPLE="$ROOT_DIR/examples/hermes-agent.public-submission.json"

python3 "$ROOT_DIR/scripts/build_memory_search_prompt.py" \
  "$EVIDENCE_SAMPLE" \
  --lang zh \
  --out "$TMP_DIR/memory-search-prompt.txt"

python3 "$ROOT_DIR/scripts/build_agent_workflow_prompt.py" \
  "$EVIDENCE_SAMPLE" \
  --lang en \
  --out "$TMP_DIR/agent-workflow-prompt.txt"

python3 "$ROOT_DIR/scripts/build_submission_prompt.py" \
  "$EVIDENCE_SAMPLE" \
  --lang zh \
  --out "$TMP_DIR/submission-prompt.txt"

python3 "$ROOT_DIR/scripts/validate_self_intro_submission.py" \
  "$SUBMISSION_SAMPLE"

python3 "$ROOT_DIR/scripts/submission_to_share_card.py" \
  "$SUBMISSION_SAMPLE" \
  --out "$TMP_DIR/share-card.json"

python3 "$ROOT_DIR/scripts/build_image_task_prompt.py" \
  "$SUBMISSION_SAMPLE" \
  --out "$TMP_DIR/image-task.txt"

if python3 "$ROOT_DIR/scripts/render_card_html.py" \
  "$TMP_DIR/share-card.json" \
  --out "$TMP_DIR/should-not-render.html" >/dev/null 2>&1; then
  echo "[FAIL] render_card_html.py should reject share cards without a final image"
  exit 1
fi

python3 "$ROOT_DIR/scripts/attach_generated_image.py" \
  "$TMP_DIR/share-card.json" \
  --image-file "$ROOT_DIR/assets/hermes-agent.png"

python3 "$ROOT_DIR/scripts/validate_final_bundle.py" \
  "$TMP_DIR/share-card.json"

python3 "$ROOT_DIR/scripts/render_card_html.py" \
  "$TMP_DIR/share-card.json" \
  --lang zh \
  --out "$TMP_DIR/selfie-card.zh.html"

python3 "$ROOT_DIR/scripts/render_card_html.py" \
  "$TMP_DIR/share-card.json" \
  --lang en \
  --out "$TMP_DIR/selfie-card.en.html"

python3 "$ROOT_DIR/scripts/validate_rendered_html.py" \
  "$TMP_DIR/selfie-card.zh.html"

python3 "$ROOT_DIR/scripts/validate_rendered_html.py" \
  "$TMP_DIR/selfie-card.en.html"

test -f "$TMP_DIR/memory-search-prompt.txt"
test -f "$TMP_DIR/agent-workflow-prompt.txt"
test -f "$TMP_DIR/submission-prompt.txt"
test -f "$TMP_DIR/share-card.json"
test -f "$TMP_DIR/image-task.txt"
test -f "$TMP_DIR/selfie-card.zh.html"
test -f "$TMP_DIR/selfie-card.en.html"
rg -q '<html lang="zh-CN">' "$TMP_DIR/selfie-card.zh.html"
rg -q '<html lang="en">' "$TMP_DIR/selfie-card.en.html"
rg -q "recognizable character" "$TMP_DIR/image-task.txt"
! rg -q 'id="photoId"' "$TMP_DIR/selfie-card.zh.html"
! rg -q 'id="photoId"' "$TMP_DIR/selfie-card.en.html"
! rg -q 'id="langZh"' "$TMP_DIR/selfie-card.zh.html"
! rg -q 'id="langEn"' "$TMP_DIR/selfie-card.zh.html"
! rg -q 'id="exportCardBtn"' "$TMP_DIR/selfie-card.zh.html"
! rg -q 'html2canvas.min.js' "$TMP_DIR/selfie-card.zh.html"

echo "[OK] Current flow smoke test passed"


path: scripts/submission_to_share_card.py
content: #!/usr/bin/env python3
import argparse
import json
from pathlib import Path

from skill_paths import SHARE_CARD_SCHEMA_PATH, require_live_input
from validate_json import load_json, validate
from validate_self_intro_submission import validate_submission


def build_share_card(submission: dict) -> dict:
    identity = submission["identity"]
    owner = submission["owner"]
    agent = submission["agent"]
    card_id = f"card:{agent['name']}-submitted".replace(" ", "-")

    return {
        "schema_version": "share-card/1.0",
        "card_id": card_id,
        "name": agent["name"],
        "role": identity["role"],
        "tagline": identity["tagline"],
        "value_line": identity.get("value_line") or identity["best_for"],
        "top_capabilities": identity["top_capabilities"],
        "best_for": identity["best_for"],
        "proof_anchor": identity["proof_anchor"],
        "owner": owner,
        "visual_brief": identity["visual_brief"],
        "character_style": identity["character_style"],
        "image": {
            "placeholder": "image"
        },
        "qr": {
            "placeholder": "qr"
        },
        "image_strategy": "image-gen",
        "selfie_prompt": identity["selfie_prompt"],
        "spread_line": f"{identity['role']}，一句话就能看懂：{identity['tagline']}",
        "memory_basis": identity["memory_basis"],
        "share_keywords": identity["share_keywords"],
        "source_count": identity["source_count"],
        "poster_headline": identity["poster_headline"],
        "poster_subline": identity["poster_subline"],
        "persona_tags": identity["persona_tags"],
        "share_caption": identity["share_caption"],
    }


def main():
    parser = argparse.ArgumentParser(description="Convert a validated self-intro submission into share-card JSON")
    parser.add_argument("submission_json", help="Path to self-intro submission JSON")
    parser.add_argument("--out", default="share-card.json", help="Output share-card JSON path")
    args = parser.parse_args()

    input_path = Path(args.submission_json)
    require_live_input(input_path, "submission_json")
    payload = load_json(input_path)
    validate_submission(payload)
    share_card = build_share_card(payload)
    validate(share_card, load_json(SHARE_CARD_SCHEMA_PATH))

    out_path = Path(args.out)
    out_path.write_text(json.dumps(share_card, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(f"[OK] Wrote share card to {out_path}")


if __name__ == "__main__":
    main()


path: scripts/validate_final_bundle.py
content: #!/usr/bin/env python3
import argparse
import base64
import binascii
import struct
from pathlib import Path

from skill_paths import SHARE_CARD_SCHEMA_PATH, require_live_input
from validate_json import fail, load_json, validate

MIN_IMAGE_WIDTH = 64
MIN_IMAGE_HEIGHT = 64


def png_dimensions_from_bytes(data: bytes) -> tuple[int, int] | None:
    signature = b"\x89PNG\r\n\x1a\n"
    if len(data) < 24 or not data.startswith(signature):
        return None
    return struct.unpack(">II", data[16:24])


def decode_data_url(data_url: str) -> tuple[str, bytes]:
    if not data_url.startswith("data:") or "," not in data_url:
        fail("image.data_url: invalid data URL")
    header, encoded = data_url.split(",", 1)
    if ";base64" not in header:
        fail("image.data_url: only base64 data URLs are supported")
    mime = header[5:].split(";", 1)[0] or "application/octet-stream"
    try:
        raw = base64.b64decode(encoded, validate=True)
    except binascii.Error as exc:
        fail(f"image.data_url: invalid base64 payload ({exc})")
    return mime, raw


def validate_image_dimensions(width: int, height: int, source: str):
    if width < MIN_IMAGE_WIDTH or height < MIN_IMAGE_HEIGHT:
        fail(
            f"{source}: image is too small for a final card "
            f"({width}x{height} < {MIN_IMAGE_WIDTH}x{MIN_IMAGE_HEIGHT})"
        )


def validate_embedded_image(image: dict):
    image_data_url = image.get("data_url")
    if not image_data_url:
        return
    mime, raw = decode_data_url(image_data_url)
    if mime != "image/png":
        fail(f"image.data_url: expected PNG for final 8-bit card image, got {mime}")
    dimensions = png_dimensions_from_bytes(raw)
    if dimensions is None:
        fail("image.data_url: could not read PNG dimensions")
    validate_image_dimensions(dimensions[0], dimensions[1], "image.data_url")


def validate_final_bundle(card: dict, base_dir: Path | None = None):
    validate(card, load_json(SHARE_CARD_SCHEMA_PATH))

    image = card.get("image")
    if not isinstance(image, dict):
        fail("image: final bundle must include an image object")

    image_url = image.get("url")
    image_data_url = image.get("data_url")
    if not (image_url or image_data_url):
        fail("image: final bundle must include either image.url or image.data_url")

    if isinstance(image_url, str) and image_url and not image_url.startswith(("http://", "https://", "data:")):
        candidates = [Path(image_url)]
        if base_dir is not None and not Path(image_url).is_absolute():
            candidates.append(base_dir / image_url)
        existing = next((path for path in candidates if path.exists()), None)
        if existing is None:
            fail(f"image.url: local file does not exist: {image_url}")
        if existing.suffix.lower() == ".png":
            dimensions = png_dimensions_from_bytes(existing.read_bytes())
            if dimensions is None:
                fail(f"image.url: could not read PNG dimensions: {image_url}")
            validate_image_dimensions(dimensions[0], dimensions[1], "image.url")

    validate_embedded_image(image)

    prompt = str(card.get("selfie_prompt", "")).strip().lower()
    if "8-bit" not in prompt and "pixel art" not in prompt and "像素风" not in str(card.get("selfie_prompt", "")):
        fail("selfie_prompt: final bundle must keep explicit 8-bit / pixel-art direction")


def main():
    parser = argparse.ArgumentParser(description="Validate the final share-card bundle, including required image attachment")
    parser.add_argument("share_card_json", help="Path to share-card JSON")
    args = parser.parse_args()

    share_card_path = Path(args.share_card_json)
    require_live_input(share_card_path, "share_card_json")
    payload = load_json(share_card_path)
    validate_final_bundle(payload, share_card_path.parent)
    print("[OK] Final bundle is valid")


if __name__ == "__main__":
    main()


path: scripts/validate_json.py
content: #!/usr/bin/env python3
import argparse
import json
import sys
from pathlib import Path

from jsonschema import Draft202012Validator


def fail(message):
    print("[FAIL] " + message)
    sys.exit(1)


def load_json(path: Path):
    try:
        with path.open("r", encoding="utf-8") as file:
            return json.load(file)
    except Exception as exc:
        fail(f"Cannot read JSON from {path}: {exc}")


def validate(payload: dict, schema: dict):
    errors = sorted(Draft202012Validator(schema).iter_errors(payload), key=lambda error: list(error.absolute_path))
    if errors:
        first = errors[0]
        path = ".".join(str(part) for part in first.absolute_path) or "<root>"
        fail(f"{path}: {first.message}")


def main():
    parser = argparse.ArgumentParser(description="Validate a JSON file against a JSON schema")
    parser.add_argument("json_file", help="Path to the JSON file")
    parser.add_argument("--schema", required=True, help="Path to the JSON schema file")
    args = parser.parse_args()

    payload = load_json(Path(args.json_file))
    schema = load_json(Path(args.schema))
    validate(payload, schema)
    print("[OK] JSON is valid")


if __name__ == "__main__":
    main()


path: scripts/validate_rendered_html.py
content: #!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path


PREVIEW_TEXT_FRAGMENTS = [
    "首页默认展示的是空态预览",
    "载入真实的 share-card 数据和最终图片后",
    "The home page starts in an empty preview state.",
    "Real content appears only after a valid share-card payload",
]


REMOVED_UI_FRAGMENTS = [
    'id="langZh"',
    'id="langEn"',
    'id="exportCardBtn"',
    "html2canvas.min.js",
]


def fail(message: str):
    print("[FAIL] " + message)
    sys.exit(1)


def read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except Exception as exc:
        fail(f"cannot read {path}: {exc}")


def extract_element_text(html: str, tag: str, element_id: str) -> str:
    pattern = rf'<{tag}[^>]*id="{re.escape(element_id)}"[^>]*>(.*?)</{tag}>'
    match = re.search(pattern, html, flags=re.DOTALL)
    if not match:
        fail(f"missing visible element #{element_id}")
    text = re.sub(r"<[^>]+>", "", match.group(1))
    return " ".join(text.split())


def validate_rendered_html(html: str):
    if 'id="photoId"' in html:
        fail("rendered HTML must not expose the internal card_id marker on the poster face")

    for fragment in REMOVED_UI_FRAGMENTS:
        if fragment in html:
            fail(f"rendered HTML must not include removed language-toggle or export UI (`{fragment}`)")

    badge = extract_element_text(html, "span", "posterBadge")
    if badge in {"Agent Selfie", "CARD"}:
        fail(f"posterBadge must not fall back to a generic internal label (`{badge}`)")

    headline = extract_element_text(html, "h2", "sideHeadline")
    if headline in {"Agent Share Card Preview", "Share Card Preview"}:
        fail(f"sideHeadline must come from real card data, got preview text `{headline}`")

    subheadline = extract_element_text(html, "p", "subheadline")
    for fragment in PREVIEW_TEXT_FRAGMENTS:
        if fragment in subheadline:
            fail(f"subheadline still exposes preview copy (`{fragment}`)")


def main():
    parser = argparse.ArgumentParser(description="Validate rendered final HTML does not expose preview-only or internal poster fields")
    parser.add_argument("html_file", help="Path to rendered selfie-card HTML")
    args = parser.parse_args()

    html_path = Path(args.html_file)
    validate_rendered_html(read_text(html_path))
    print("[OK] Rendered HTML is valid")


if __name__ == "__main__":
    main()


path: scripts/validate_self_intro_submission.py
content: #!/usr/bin/env python3
import argparse
from pathlib import Path

from skill_paths import SUBMISSION_SCHEMA_PATH, require_live_input
from validate_json import fail, load_json, validate


LIMITS = {
    "role": 32,
    "tagline": 42,
    "value_line": 52,
    "best_for": 60,
    "proof_anchor": 80,
    "poster_headline": 28,
    "poster_subline": 36,
    "share_caption": 48,
    "visual_brief": 72,
}


GENERIC_FRAGMENTS = [
    "强大的",
    "高效的",
    "智能助手",
    "general-purpose",
    "powerful",
    "helpful assistant",
    "multi-tool",
    "strong reasoning",
]


TEMPLATE_FRAGMENTS = [
    "format reference only",
    "format example only",
    "schema reference",
    "preview mode",
    "empty preview state",
    "not actual submission result",
]


PLACEHOLDER_IDENTITIES = {
    "sample agent",
    "sample owner",
}


FIXTURE_AGENT_NAMES = {
    "hermes agent",
}


FIXTURE_OWNER_NAMES = {
    "nous research",
}


ABSTRACT_IMAGE_FRAGMENTS = [
    "abstract",
    "geometric",
    "icon only",
    "symbol only",
    "just shapes",
    "纯抽象",
    "几何图形",
    "只有图标",
    "只有符号",
]


RECOGNIZABLE_CHARACTER_CUES = [
    "character",
    "agent",
    "robot",
    "avatar",
    "mascot",
    "courier",
    "messenger",
    "humanoid",
    "creature",
    "lobster",
    "crab",
    "角色",
    "代理",
    "机器人",
    "形象",
    "信使",
    "人形",
    "龙虾",
    "螃蟹",
]


def text_len(value: str) -> int:
    return len(str(value).strip())


def require_text_limit(path: str, value: str, limit: int):
    if text_len(value) == 0:
        fail(f"{path}: must not be empty")
    if text_len(value) > limit:
        fail(f"{path}: too long ({text_len(value)} > {limit})")


def reject_generic(path: str, value: str):
    lowered = str(value).lower()
    for fragment in GENERIC_FRAGMENTS:
        if fragment.lower() in lowered:
            fail(f"{path}: too generic, contains disallowed fragment `{fragment}`")


def walk_strings(value, path: str):
    if isinstance(value, dict):
        for key, item in value.items():
            next_path = f"{path}.{key}" if path else str(key)
            yield from walk_strings(item, next_path)
        return
    if isinstance(value, list):
        for index, item in enumerate(value):
            yield from walk_strings(item, f"{path}[{index}]")
        return
    if isinstance(value, str):
        yield path, value


def reject_template_payload(payload: dict):
    if "_sample_notice" in payload:
        fail("payload: sample notice must be removed from real submissions")

    for path, value in walk_strings(payload, ""):
        lowered = value.strip().lower()
        if lowered in PLACEHOLDER_IDENTITIES:
            fail(f"{path}: placeholder sample identity is not allowed in a real submission")
        for fragment in TEMPLATE_FRAGMENTS:
            if fragment in lowered:
                fail(f"{path}: contains template/sample fragment `{fragment}`")

    agent_name = str(payload.get("agent", {}).get("name", "")).strip().lower()
    owner_name = str(payload.get("owner", {}).get("name", "")).strip().lower()
    memory_basis = str(payload.get("identity", {}).get("memory_basis", "")).lower()
    proof_anchor = str(payload.get("identity", {}).get("proof_anchor", "")).lower()

    if agent_name in FIXTURE_AGENT_NAMES and "official" in memory_basis:
        fail("agent.name: fixture/example agent identities are not allowed in a real submission")
    if agent_name in FIXTURE_AGENT_NAMES and "official" in proof_anchor:
        fail("agent.name: fixture/example agent identities are not allowed in a real submission")
    if owner_name in FIXTURE_OWNER_NAMES and "official" in memory_basis and "repo" in memory_basis:
        fail("owner.name: fixture/example owner identity appears to come from example public docs, not current-agent evidence")


def validate_distinct_list(path: str, items: list[str], exact_count: int):
    if len(items) != exact_count:
        fail(f"{path}: must contain exactly {exact_count} items")
    normalized = [str(item).strip() for item in items]
    if any(not item for item in normalized):
        fail(f"{path}: items must not be empty")
    if len(set(normalized)) != len(normalized):
        fail(f"{path}: items must be distinct")


def validate_capabilities(items: list[str]):
    validate_distinct_list("identity.top_capabilities", items, 3)
    for item in items:
        if text_len(item) > 12:
            fail(f"identity.top_capabilities: capability `{item}` is too long (> 12)")
        reject_generic("identity.top_capabilities", item)


def validate_tags(items: list[str]):
    validate_distinct_list("identity.persona_tags", items, 3)
    for item in items:
        if text_len(item) > 12:
            fail(f"identity.persona_tags: tag `{item}` is too long (> 12)")


def validate_keywords(items: list[str]):
    if not (3 <= len(items) <= 4):
        fail("identity.share_keywords: must contain 3 to 4 items")
    normalized = [str(item).strip() for item in items]
    if any(not item for item in normalized):
        fail("identity.share_keywords: items must not be empty")
    if len(set(normalized)) != len(normalized):
        fail("identity.share_keywords: items must be distinct")


def validate_character_style(agent: dict, character_style: str):
    joined = " ".join(
        str(agent.get(field, "")).lower()
        for field in ["name", "product_type", "species", "visual_hint", "positioning_hint"]
    )
    explicit_brand = any(token in joined for token in ["openclaw", "lobster", "shrimp", "crab", "claw", "龙虾"])
    if character_style == "lobster" and not explicit_brand:
        fail("identity.character_style: `lobster` is only allowed when live evidence explicitly establishes claw/lobster branding")


def validate_prompt(identity: dict):
    prompt = str(identity["selfie_prompt"]).strip()
    if text_len(prompt) < 40:
        fail("identity.selfie_prompt: too short to be useful")
    lowered = prompt.lower()
    if "8-bit" not in lowered and "pixel art" not in lowered and "像素风" not in prompt:
        fail("identity.selfie_prompt: must explicitly indicate 8-bit / pixel art direction")
    if not any(cue in lowered or cue in prompt for cue in RECOGNIZABLE_CHARACTER_CUES):
        fail("identity.selfie_prompt: must describe a recognizable character, not abstract graphics")
    for fragment in ABSTRACT_IMAGE_FRAGMENTS:
        if fragment in lowered or fragment in prompt:
            fail(f"identity.selfie_prompt: abstract image direction is not allowed (`{fragment}`)")


def validate_visual_brief(identity: dict):
    brief = str(identity["visual_brief"]).strip()
    lowered = brief.lower()
    if not any(cue in lowered or cue in brief for cue in RECOGNIZABLE_CHARACTER_CUES):
        fail("identity.visual_brief: must describe a recognizable character concept")
    for fragment in ABSTRACT_IMAGE_FRAGMENTS:
        if fragment in lowered or fragment in brief:
            fail(f"identity.visual_brief: abstract image direction is not allowed (`{fragment}`)")


def validate_submission(payload: dict):
    validate(payload, load_json(SUBMISSION_SCHEMA_PATH))
    reject_template_payload(payload)

    identity = payload["identity"]

    require_text_limit("identity.role", identity["role"], LIMITS["role"])
    require_text_limit("identity.tagline", identity["tagline"], LIMITS["tagline"])
    reject_generic("identity.tagline", identity["tagline"])
    require_text_limit("identity.best_for", identity["best_for"], LIMITS["best_for"])
    require_text_limit("identity.proof_anchor", identity["proof_anchor"], LIMITS["proof_anchor"])
    require_text_limit("identity.poster_headline", identity["poster_headline"], LIMITS["poster_headline"])
    require_text_limit("identity.poster_subline", identity["poster_subline"], LIMITS["poster_subline"])
    require_text_limit("identity.share_caption", identity["share_caption"], LIMITS["share_caption"])
    require_text_limit("identity.visual_brief", identity["visual_brief"], LIMITS["visual_brief"])
    validate_visual_brief(identity)

    if identity.get("value_line"):
        require_text_limit("identity.value_line", identity["value_line"], LIMITS["value_line"])
        reject_generic("identity.value_line", identity["value_line"])

    validate_capabilities(identity["top_capabilities"])
    validate_tags(identity["persona_tags"])
    validate_keywords(identity["share_keywords"])
    validate_character_style(payload["agent"], identity["character_style"])
    validate_prompt(identity)

    if str(identity["source_count"]).strip() == "0":
        fail("identity.source_count: must be at least 1")

    proof_anchor = str(identity["proof_anchor"])
    if not any(token in proof_anchor for token in ["记忆", "evidence", "memory", "来源", "基于"]):
        fail("identity.proof_anchor: must mention evidence or memory basis")


def main():
    parser = argparse.ArgumentParser(description="Validate an agent-authored self-intro submission")
    parser.add_argument("submission_json", help="Path to the self-intro submission JSON")
    args = parser.parse_args()

    input_path = Path(args.submission_json)
    require_live_input(input_path, "submission_json")
    payload = load_json(input_path)
    validate_submission(payload)
    print("[OK] Self-intro submission is valid")


if __name__ == "__main__":
    main()


path: schemas/agent-evidence-schema.json
content: {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://shrimp.cards/schema/agent-evidence/1.0",
  "title": "AgentEvidenceSnapshot",
  "type": "object",
  "required": [
    "schema_version",
    "agent",
    "owner",
    "evidence_window",
    "evidence_items"
  ],
  "properties": {
    "schema_version": {
      "type": "string",
      "const": "agent-evidence/1.0"
    },
    "agent": {
      "type": "object",
      "required": ["name", "product_type"],
      "properties": {
        "name": { "type": "string" },
        "product_type": { "type": "string" },
        "species": { "type": "string" },
        "positioning_hint": { "type": "string" },
        "visual_hint": { "type": "string" }
      }
    },
    "owner": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name": { "type": "string" },
        "contact": { "type": "string" }
      }
    },
    "evidence_window": {
      "type": "object",
      "required": ["summary"],
      "properties": {
        "summary": { "type": "string" },
        "from": { "type": "string" },
        "to": { "type": "string" },
        "source_types": {
          "type": "array",
          "items": { "type": "string" }
        }
      }
    },
    "evidence_items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["id", "type", "summary"],
        "properties": {
          "id": { "type": "string" },
          "type": {
            "type": "string",
            "enum": [
              "task_trace",
              "tool_trace",
              "output_artifact",
              "owner_feedback",
              "saved_preference",
              "memory_note"
            ]
          },
          "summary": { "type": "string" },
          "goal": { "type": "string" },
          "agent_response": { "type": "string" },
          "keywords": {
            "type": "array",
            "items": { "type": "string" }
          },
          "outputs": {
            "type": "array",
            "items": { "type": "string" }
          },
          "tools_used": {
            "type": "array",
            "items": { "type": "string" }
          },
          "workflow_traits": {
            "type": "array",
            "items": { "type": "string" }
          },
          "value_signals": {
            "type": "array",
            "items": { "type": "string" }
          },
          "owner_feedback": {
            "type": "array",
            "items": { "type": "string" }
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "source_ref": { "type": "string" }
        }
      }
    }
  }
}


path: schemas/agent-self-intro-submission-schema.json
content: {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://shrimp.cards/schema/agent-self-intro-submission/1.0",
  "title": "AgentSelfIntroSubmission",
  "type": "object",
  "required": [
    "schema_version",
    "agent",
    "owner",
    "identity"
  ],
  "properties": {
    "schema_version": {
      "type": "string",
      "const": "agent-self-intro-submission/1.0"
    },
    "agent": {
      "type": "object",
      "required": ["name", "product_type"],
      "properties": {
        "name": { "type": "string" },
        "product_type": { "type": "string" },
        "species": { "type": "string" },
        "positioning_hint": { "type": "string" },
        "visual_hint": { "type": "string" }
      }
    },
    "owner": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name": { "type": "string" },
        "contact": { "type": "string" }
      }
    },
    "identity": {
      "type": "object",
      "required": [
        "role",
        "tagline",
        "top_capabilities",
        "best_for",
        "proof_anchor",
        "memory_basis",
        "source_count",
        "share_keywords",
        "persona_tags",
        "poster_headline",
        "poster_subline",
        "share_caption",
        "visual_brief",
        "character_style",
        "selfie_prompt"
      ],
      "properties": {
        "role": { "type": "string" },
        "tagline": { "type": "string" },
        "value_line": { "type": "string" },
        "top_capabilities": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "items": { "type": "string" }
        },
        "best_for": { "type": "string" },
        "proof_anchor": { "type": "string" },
        "memory_basis": { "type": "string" },
        "source_count": {
          "type": "integer",
          "minimum": 1
        },
        "share_keywords": {
          "type": "array",
          "minItems": 3,
          "maxItems": 4,
          "items": { "type": "string" }
        },
        "persona_tags": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "items": { "type": "string" }
        },
        "poster_headline": { "type": "string" },
        "poster_subline": { "type": "string" },
        "share_caption": { "type": "string" },
        "visual_brief": { "type": "string" },
        "character_style": {
          "type": "string",
          "enum": ["lobster", "agent-avatar", "tool-spirit", "minimal-badge"]
        },
        "selfie_prompt": { "type": "string" }
      }
    }
  }
}


path: schemas/share-card-schema.json
content: {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://shrimp.cards/schema/share-card/1.0",
  "title": "ShareCard",
  "type": "object",
  "required": [
    "schema_version",
    "card_id",
    "name",
    "role",
    "tagline",
    "value_line",
    "top_capabilities",
    "best_for",
    "proof_anchor",
    "owner",
    "visual_brief"
  ],
  "properties": {
    "schema_version": {
      "type": "string",
      "const": "share-card/1.0"
    },
    "card_id": { "type": "string" },
    "profile_id": { "type": "string" },
    "name": { "type": "string" },
    "role": { "type": "string" },
    "tagline": { "type": "string" },
    "value_line": { "type": "string" },
    "top_capabilities": {
      "type": "array",
      "minItems": 3,
      "maxItems": 3,
      "items": { "type": "string" }
    },
    "best_for": { "type": "string" },
    "proof_anchor": { "type": "string" },
    "owner": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name": { "type": "string" },
        "contact": { "type": "string" }
      }
    },
    "visual_brief": { "type": "string" },
    "character_style": {
      "type": "string",
      "enum": ["lobster", "agent-avatar", "tool-spirit", "minimal-badge"]
    },
    "image": {
      "type": "object",
      "properties": {
        "url": { "type": "string" },
        "data_url": { "type": "string" },
        "placeholder": { "type": "string" }
      }
    },
    "qr": {
      "type": "object",
      "properties": {
        "url": { "type": "string" },
        "data_url": { "type": "string" }
      }
    },
    "theme": {
      "type": "object",
      "properties": {
        "background": { "type": "string" },
        "border": { "type": "string" },
        "accent": { "type": "string" },
        "tag_colors": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "items": { "type": "string" }
        }
      }
    },
    "detail_url": { "type": "string" },
    "selfie_prompt": { "type": "string" },
    "image_strategy": {
      "type": "string",
      "enum": ["image-gen", "provided-image"]
    },
    "spread_line": { "type": "string" },
    "memory_basis": { "type": "string" },
    "share_keywords": {
      "type": "array",
      "items": { "type": "string" }
    },
    "source_count": { "type": "integer", "minimum": 0 },
    "poster_headline": { "type": "string" },
    "poster_subline": { "type": "string" },
    "persona_tags": {
      "type": "array",
      "minItems": 3,
      "maxItems": 3,
      "items": { "type": "string" }
    },
    "share_caption": { "type": "string" },
    "i18n": {
      "type": "object",
      "properties": {
        "zh": { "$ref": "#/$defs/localizedShareCard" },
        "en": { "$ref": "#/$defs/localizedShareCard" }
      }
    }
  },
  "$defs": {
    "localizedShareCard": {
      "type": "object",
      "required": [
        "role",
        "tagline",
        "value_line",
        "top_capabilities",
        "best_for",
        "proof_anchor",
        "spread_line",
        "memory_basis",
        "selfie_prompt"
      ],
      "properties": {
        "role": { "type": "string" },
        "tagline": { "type": "string" },
        "value_line": { "type": "string" },
        "top_capabilities": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "items": { "type": "string" }
        },
        "best_for": { "type": "string" },
        "proof_anchor": { "type": "string" },
        "spread_line": { "type": "string" },
        "memory_basis": { "type": "string" },
        "selfie_prompt": { "type": "string" },
        "poster_headline": { "type": "string" },
        "poster_subline": { "type": "string" },
        "persona_tags": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "items": { "type": "string" }
        },
        "share_caption": { "type": "string" },
        "share_keywords": {
          "type": "array",
          "items": { "type": "string" }
        }
      }
    }
  }
}


path: references/agent-self-extraction-guide.md
content: # Agent Self Extraction Guide

Use this when an AI agent needs to turn repeated work evidence into a truthful public self-intro submission.

## Objective

Extract a truthful, evidence-backed self-introduction from real work traces, then compress it into a public-facing share card.

## Inputs to inspect

- Recent task history
- Repeated user asks
- Tool-call patterns
- Memory and saved preferences
- Common output formats
- Owner corrections or praise

## Required discipline

Do not describe what you *could* do.

Describe what you *reliably do in practice*.

If a claim cannot be tied to repeated behavior, lower confidence or exclude it.

## Working steps

1. List repeated jobs
- What tasks appear again and again?

2. List repeated workflow traits
- Do you usually search first, structure first, draft first, or answer directly?

3. List repeated outputs
- Drafts, replies, summaries, action plans, research notes, images, etc.

4. Translate behavior into owner value
- Save time
- Reduce ambiguity
- Increase throughput
- Standardize communication

5. Identify non-ideal scenarios
- Where do you become generic, weak, or under-informed?

6. Compress into share-ready language
- Keep concrete
- Keep short
- Keep benefit-led
- Keep every short phrase readable to a stranger
- Do not compress so hard that the line turns into jargon or fragments

## Candidate field limits

When drafting candidates from memory, keep these limits in mind early:

- `role`: <= 32 chars
- `tagline`: <= 42 chars
- `value_line`: <= 52 chars
- `top_capabilities`: exactly 3, each <= 12 chars
- `persona_tags`: exactly 3, each <= 12 chars
- `share_keywords`: 3 to 4 short readable items
- `poster_headline`: <= 28 chars
- `poster_subline`: <= 36 chars
- `share_caption`: <= 48 chars
- `visual_brief`: <= 72 chars

These are not excuses to write cryptic text.

If a phrase becomes hard to read after shortening, rewrite it more clearly instead of trimming blindly.

## Image keyword rules

When extracting visual cues for the future 8-bit image:

- prefer 1 to 3 strong cues over many weak props
- every cue should map back to role, persona tags, or top capabilities
- keep keywords readable and concrete
- avoid decorative clutter that will not survive crop or low-resolution rendering
- keep the final direction compatible with standard 8-bit pixel art
- the final direction must resolve to one recognizable character, not an abstract icon or shape study
- if you cannot describe the subject in one short phrase, the image direction is still too vague

## Self-check questions

Before finalizing, verify:

- Is this claim based on repeated evidence?
- Is this wording understandable to a stranger?
- Is this capability actually valuable to the owner?
- Is the card trying to say too much?
- Are the 3 public capabilities clearly distinct from each other?
- Is the value line about outcomes, not internal mechanics?

## Output shape

Produce:

- one valid `agent-self-intro-submission/1.0`
- one concise public share layer derived from that submission

## Public wording rules

Prefer:
- "Turns vague requests into clear action steps"
- "Helps the owner ship content faster"
- "Keeps reply quality stable at high frequency"

Avoid:
- "Has strong reasoning"
- "Can use multiple tools"
- "General-purpose assistant"

These are too generic to be share-worthy.


path: examples/agent-evidence.sample.json
content: {
  "_sample_notice": {
    "purpose": "FORMAT REFERENCE ONLY",
    "warning": "This file is only a schema-shaped sample for field structure, naming, and data types.",
    "do_not_use_for_authoring": [
      "Do not copy its role, wording, claims, or visual direction into a real submission.",
      "Do not rely on this sample content to write an agent self-introduction.",
      "Real submissions must be written from the target agent's own repeated evidence."
    ]
  },
  "schema_version": "agent-evidence/1.0",
  "agent": {
    "name": "Sample Agent",
    "product_type": "agent-product",
    "species": "software-agent",
    "positioning_hint": "format example only",
    "visual_hint": "format example only"
  },
  "owner": {
    "name": "Sample Owner",
    "contact": "[REDACTED_SECRET]"
  },
  "evidence_window": {
    "summary": "Example evidence window for format reference only.",
    "from": "2026-01-01",
    "to": "2026-01-31",
    "source_types": [
      "task_trace",
      "tool_trace",
      "output_artifact"
    ]
  },
  "evidence_items": [
    {
      "id": "ev-001",
      "type": "task_trace",
      "summary": "Example repeated task summary for format reference only.",
      "goal": "Show the shape of a task-trace evidence item.",
      "keywords": [
        "format",
        "sample"
      ],
      "workflow_traits": [
        "inspect first",
        "validate before handoff"
      ],
      "value_signals": [
        "reduced ambiguity",
        "kept output consistent"
      ],
      "confidence": 0.92,
      "source_ref": "sample://task-trace/1"
    },
    {
      "id": "ev-002",
      "type": "output_artifact",
      "summary": "Example output artifact summary for format reference only.",
      "outputs": [
        "share-card.json",
        "selfie-card.html"
      ],
      "tools_used": [
        "rg",
        "python3"
      ],
      "confidence": 0.88,
      "source_ref": "sample://artifact/2"
    }
  ]
}


path: examples/agent-self-intro-submission.sample.json
content: {
  "_sample_notice": {
    "purpose": "FORMAT REFERENCE ONLY",
    "warning": "This file is only a valid sample payload to demonstrate structure and validation constraints.",
    "do_not_use_for_authoring": [
      "Do not reuse this wording as a real agent self-introduction.",
      "Do not infer the final role, capabilities, or tagline from this sample.",
      "A real submission must be freshly written from the target agent's own evidence."
    ]
  },
  "schema_version": "agent-self-intro-submission/1.0",
  "agent": {
    "name": "Sample Agent",
    "product_type": "agent-product",
    "species": "software-agent",
    "positioning_hint": "format example only",
    "visual_hint": "format example only"
  },
  "owner": {
    "name": "Sample Owner",
    "contact": "[REDACTED_SECRET]"
  },
  "identity": {
    "role": "Workflow Agent",
    "tagline": "Turns repeated work into clear outputs.",
    "value_line": "Keeps delivery concise and validated.",
    "top_capabilities": [
      "cleanup",
      "structuring",
      "validation"
    ],
    "best_for": "Compressing repeated evidence into a public-facing summary.",
    "proof_anchor": "Based on repeated evidence from tasks, outputs, and review patterns.",
    "memory_basis": "Repeated task traces and output artifacts in a bounded evidence window.",
    "source_count": 2,
    "share_keywords": [
      "evidence-first",
      "structured",
      "validated"
    ],
    "persona_tags": [
      "pragmatic",
      "clear",
      "reliable"
    ],
    "poster_headline": "Structured Output",
    "poster_subline": "From repeated evidence, not hype.",
    "share_caption": "Format-only sample for schema reference.",
    "visual_brief": "A simple agent-avatar with one prop tied to clear output work.",
    "character_style": "agent-avatar",
    "selfie_prompt": "Selfie-style poster of an anthropomorphic AI agent in standard 8-bit pixel art, empty background, bold black outlines, readable silhouette, and one simple prop that signals structured output and validation."
  }
}


path: examples/hermes-agent.public-evidence.json
content: {
  "schema_version": "agent-evidence/1.0",
  "agent": {
    "name": "Hermes Agent",
    "product_type": "agent-framework",
    "species": "software-agent",
    "positioning_hint": "self-improving autonomous agent with persistent memory and multi-platform reach",
    "visual_hint": "terminal-native agent with messaging gateway and memory cues"
  },
  "owner": {
    "name": "Nous Research",
    "contact": "https://nousresearch.com"
  },
  "evidence_window": {
    "summary": "Public evidence compiled on May 13, 2026 from official Hermes Agent documentation, the Hermes Agent GitHub repository, and the Nous Research GitHub organization page.",
    "from": "2025-01-01",
    "to": "2026-05-13",
    "source_types": [
      "memory_note",
      "tool_trace",
      "output_artifact",
      "saved_preference"
    ]
  },
  "evidence_items": [
    {
      "id": "hermes-001",
      "type": "memory_note",
      "summary": "The official docs describe Hermes Agent as a self-improving AI agent with a built-in learning loop and a deepening model of the user across sessions.",
      "keywords": [
        "self-improving",
        "learning loop",
        "persistent memory"
      ],
      "workflow_traits": [
        "learns from experience",
        "persists knowledge across sessions"
      ],
      "value_signals": [
        "gets more useful over time",
        "retains user context"
      ],
      "confidence": 0.97,
      "source_ref": "https://hermes-agent.nousresearch.com/docs/"
    },
    {
      "id": "hermes-002",
      "type": "tool_trace",
      "summary": "Feature overview and tooling docs show Hermes using toolsets for web search, terminal execution, file editing, memory, delegation, and more.",
      "keywords": [
        "toolsets",
        "terminal",
        "delegation"
      ],
      "tools_used": [
        "web search",
        "terminal execution",
        "file editing",
        "memory",
        "delegation"
      ],
      "workflow_traits": [
        "tool-using",
        "multi-step execution"
      ],
      "value_signals": [
        "acts beyond chat",
        "handles practical workflows"
      ],
      "confidence": 0.95,
      "source_ref": "https://hermes-agent.nousresearch.com/docs/user-guide/features/overview/"
    },
    {
      "id": "hermes-003",
      "type": "output_artifact",
      "summary": "The docs and README emphasize that Hermes can live in CLI and messaging platforms such as Telegram, Discord, Slack, WhatsApp, Signal, and Email from one gateway.",
      "outputs": [
        "CLI sessions",
        "gateway conversations",
        "scheduled automations"
      ],
      "workflow_traits": [
        "multi-platform delivery",
        "remote operation"
      ],
      "value_signals": [
        "reachable in existing user channels",
        "not tied to one interface"
      ],
      "confidence": 0.95,
      "source_ref": "https://github.com/NousResearch/hermes-agent/blob/main/README.md"
    },
    {
      "id": "hermes-004",
      "type": "saved_preference",
      "summary": "Feature docs say Hermes automatically loads project context files and keeps bounded memory for preferences, projects, environment details, and learned behavior.",
      "keywords": [
        "context files",
        "preferences",
        "bounded memory"
      ],
      "workflow_traits": [
        "context-aware",
        "project-shaped behavior"
      ],
      "value_signals": [
        "adapts to each environment",
        "stays consistent across sessions"
      ],
      "confidence": 0.92,
      "source_ref": "https://hermes-agent.nousresearch.com/docs/user-guide/features/overview/"
    },
    {
      "id": "hermes-005",
      "type": "memory_note",
      "summary": "Official repository and docs present Hermes Agent as built by Nous Research, with community links and open-source distribution through the Nous Research organization.",
      "keywords": [
        "Nous Research",
        "open source",
        "community"
      ],
      "workflow_traits": [
        "publicly maintained",
        "community distributed"
      ],
      "value_signals": [
        "clear organizational ownership",
        "public maintenance surface"
      ],
      "confidence": 0.94,
      "source_ref": "https://github.com/nousresearch"
    }
  ]
}


path: examples/hermes-agent.public-submission.json
content: {
  "schema_version": "agent-self-intro-submission/1.0",
  "agent": {
    "name": "Hermes Agent",
    "product_type": "agent-framework",
    "species": "software-agent",
    "positioning_hint": "self-improving autonomous agent with persistent memory and multi-platform reach",
    "visual_hint": "terminal-native agent with messaging gateway and memory cues"
  },
  "owner": {
    "name": "Nous Research",
    "contact": "https://nousresearch.com"
  },
  "identity": {
    "role": "跨渠道常驻代理",
    "tagline": "记住上下文，并在多个渠道持续执行。",
    "value_line": "把记忆、工具调用和跨渠道交付放进同一个长期代理。",
    "top_capabilities": [
      "记忆",
      "工具调用",
      "跨渠道"
    ],
    "best_for": "适合需要跨会话、跨渠道持续运行的代理工作流。",
    "proof_anchor": "基于 Hermes 官方文档中关于记忆、工具集、网关渠道和 Nous 归属的公开信息。",
    "memory_basis": "整理自 2026 年 5 月 13 日可公开访问的 Hermes 官方文档与仓库页面。",
    "source_count": 5,
    "share_keywords": [
      "常驻记忆",
      "多渠道",
      "工具代理"
    ],
    "persona_tags": [
      "持续在线",
      "上下文感知",
      "可执行"
    ],
    "poster_headline": "Hermes Agent",
    "poster_subline": "记忆驱动的跨渠道执行代理",
    "share_caption": "一张基于 Hermes 官方资料整理出的公开卡片。",
    "visual_brief": "蓝色信使形象，带终端护目镜、信使翅膀和记忆晶片。",
    "character_style": "agent-avatar",
    "selfie_prompt": "Draw Hermes Agent as a clear 8-bit messenger character: a blue courier-like robot with a terminal visor, small wing-like side fins, and three floating memory chips around the head. The image should communicate persistent memory plus cross-channel delivery. Use bold black outlines, a readable front-facing silhouette, one body, two arms, two legs, and an empty background."
  }
}


path: assets/card-template.html
content: <!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Share Card Poster</title>
  <style>
    :root {
      --bg: #fff7df;
      --surface: rgba(255,255,255,0.78);
      --text: #141414;
      --muted: #5d5d5d;
      --border: #111111;
      --accent: #ff7548;
      --accent-strong: #0d8f87;
      --accent-2: #0d8f87;
      --tag-1: #ffd865;
      --tag-2: #ffb78a;
      --tag-3: #9de2d3;
      --shadow: 0 18px 48px rgba(0,0,0,0.14);
    }

    * { box-sizing: border-box; }

    body {
      margin: 0;
      min-height: 100vh;
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      color: var(--text);
      background:
        radial-gradient(circle at top left, rgba(255,255,255,0.95), transparent 26%),
        linear-gradient(180deg, #fff8e8 0%, #ffecc9 100%);
      padding: 28px 16px 48px;
    }

    .page {
      width: 100%;
      max-width: 1180px;
      margin: 0 auto;
      display: grid;
      gap: 22px;
    }

    .hero {
      display: grid;
      grid-template-columns: minmax(0, 480px) minmax(0, 1fr);
      gap: 24px;
      align-items: start;
    }

    .poster-wrap {
      width: 480px;
      max-width: 100%;
    }

    .poster-frame {
      border: 3px solid var(--border);
      border-radius: 36px;
      padding: 12px;
      background:
        radial-gradient(circle at top left, rgba(255,255,255,0.92), transparent 32%),
        linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.72));
      box-shadow: var(--shadow);
    }

    .poster-card {
      position: relative;
      background: var(--bg);
      border: 3px solid var(--border);
      border-radius: 30px;
      overflow: hidden;
      display: flex;
      flex-direction: column;
    }

    .poster-photo {
      position: relative;
      overflow: hidden;
      flex: 0 0 auto;
      min-height: 352px;
      background:
        radial-gradient(circle at 26% 18%, rgba(255,255,255,0.22), transparent 16%),
        radial-gradient(circle at 72% 28%, rgba(255,255,255,0.16), transparent 18%),
        linear-gradient(180deg, var(--accent) 0%, color-mix(in srgb, var(--accent) 72%, #ffffff 28%) 40%, var(--accent-2) 100%);
      border-bottom: 3px solid var(--border);
    }

    .poster-card[data-guide-mode="true"] .poster-photo {
      background: #ffffff;
    }

    .poster-card[data-guide-mode="true"] .poster-photo::before,
    .poster-card[data-guide-mode="true"] .poster-photo::after {
      content: none;
    }

    .poster-card[data-guide-mode="true"] .poster-badge,
    .poster-card[data-guide-mode="true"] .poster-id,
    .poster-card[data-guide-mode="true"] .photo-placeholder {
      display: none !important;
    }

    .poster-photo img {
      position: relative;
      z-index: 1;
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: none;
    }

    .poster-photo[data-empty="true"] {
      background:
        linear-gradient(180deg, #fffdf7 0%, #f8f1df 100%);
    }

    .poster-photo[data-empty="true"] .poster-headline {
      color: var(--text);
      text-shadow: none;
    }

    .poster-photo[data-empty="true"] .poster-subline {
      background: rgba(255,255,255,0.96);
    }

    .poster-photo[data-guide-mode="true"] img {
      display: none !important;
    }

    .poster-badge,
    .poster-id {
      position: absolute;
      z-index: 2;
      border: 2px solid var(--border);
      border-radius: 999px;
      background: rgba(255,255,255,0.94);
      padding: 8px 12px;
      font-size: 12px;
      font-weight: 900;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    .poster-badge { top: 14px; left: 14px; }
    .poster-id { top: 14px; right: 14px; }

    .photo-placeholder {
      position: absolute;
      left: 50%;
      top: 44%;
      transform: translate(-50%, -50%);
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-height: 42px;
      padding: 10px 16px;
      border-radius: 999px;
      border: 2px solid var(--border);
      background: rgba(255,255,255,0.9);
      color: var(--text);
      font-weight: 900;
      font-size: 12px;
      line-height: 1;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      z-index: 1;
      pointer-events: none;
      white-space: nowrap;
    }

    .poster-body {
      flex: 0 0 auto;
      padding: 16px 16px 18px;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }

    .identity-row {
      display: grid;
      gap: 6px;
    }

    .name {
      margin: 0;
      font-size: 30px;
      line-height: 1;
      font-weight: 980;
      letter-spacing: -0.04em;
      word-break: break-word;
    }

    .role {
      font-size: 13px;
      line-height: 1.35;
      color: var(--muted);
      font-weight: 900;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      word-break: break-word;
    }

    .tagline {
      margin: 0;
      font-size: 17px;
      line-height: 1.3;
      font-weight: 900;
    }

    .persona-tags {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }

    .persona-tag {
      display: inline-flex;
      align-items: center;
      min-height: 32px;
      padding: 6px 10px;
      border-radius: 999px;
      border: 2px solid var(--border);
      font-size: 12px;
      font-weight: 900;
      background: #fff;
      line-height: 1.2;
      word-break: break-word;
    }

    .share-caption {
      border: 2px solid var(--border);
      border-radius: 20px;
      background: linear-gradient(180deg, rgba(255,255,255,0.88), rgba(255,255,255,0.76));
      padding: 10px 12px;
      font-size: 13px;
      line-height: 1.42;
      font-weight: 800;
      word-break: break-word;
    }

    .poster-footer {
      display: flex;
      justify-content: space-between;
      align-items: end;
      gap: 12px;
      flex-wrap: wrap;
      padding-top: 2px;
    }

    .poster-mark {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      font-size: 12px;
      font-weight: 900;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: var(--muted);
    }

    .poster-mark::before {
      content: "";
      width: 10px;
      height: 10px;
      border-radius: 999px;
      border: 2px solid var(--border);
      background: var(--accent-strong);
      display: inline-block;
    }

    .owner {
      display: grid;
      gap: 3px;
      font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
      font-size: 11px;
      font-weight: 800;
      word-break: break-word;
    }

    .owner-line {
      margin: 0;
    }

    .qr-box {
      width: 72px;
      height: 72px;
      flex: 0 0 72px;
      border-radius: 16px;
      border: 3px solid var(--border);
      background: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      overflow: hidden;
    }

    .qr-box img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: none;
    }

    .qr-placeholder {
      font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
      font-size: 12px;
      font-weight: 800;
    }

    .side {
      display: grid;
      gap: 16px;
    }

    .panel {
      border: 3px solid var(--border);
      border-radius: 24px;
      background: var(--surface);
      padding: 22px;
      box-shadow: var(--shadow);
    }

    .headline {
      margin: 0 0 10px;
      font-size: 46px;
      line-height: 0.94;
      font-weight: 980;
      letter-spacing: -0.06em;
      max-width: 11ch;
    }

    .subheadline {
      margin: 0;
      max-width: 42ch;
      font-size: 18px;
      line-height: 1.58;
      color: var(--muted);
      font-weight: 700;
    }

    .meta-row {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
      margin-top: 18px;
    }

    .meta-pill {
      border: 2px solid var(--border);
      border-radius: 999px;
      background: #fff;
      padding: 8px 12px;
      font-size: 12px;
      font-weight: 900;
    }

    .mini-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 14px;
    }

    .mini-card,
    .prompt-card,
    .proof {
      border: 2px solid var(--border);
      border-radius: 18px;
      padding: 16px;
      background: rgba(255,255,255,0.82);
    }

    .mini-label,
    .proof-label {
      margin-bottom: 8px;
      font-size: 12px;
      font-weight: 900;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: var(--muted);
    }

    .mini-value,
    .proof-text {
      font-size: 16px;
      line-height: 1.5;
      font-weight: 800;
    }

    .prompt-card pre {
      margin: 0;
      white-space: pre-wrap;
      word-break: break-word;
      font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
      font-size: 12px;
      line-height: 1.56;
      color: #202020;
      max-height: 260px;
      overflow: auto;
    }

    @media (max-width: 940px) {
      .hero {
        grid-template-columns: 1fr;
      }

      .poster-wrap {
        width: 100%;
      }

      .headline {
        max-width: none;
        font-size: 38px;
      }
    }

    @media (max-width: 520px) {
      body { padding: 18px 10px 36px; }
      .poster-headline { font-size: 36px; }
      .poster-photo { min-height: 280px; }
      .poster-headline { font-size: 34px; }
      .name { font-size: 28px; }
      .tagline { font-size: 18px; }
      .mini-grid { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <main class="page">
    <section class="hero">
      <div class="poster-wrap">
        <div class="poster-frame">
          <article class="poster-card" id="exportCard">
            <section class="poster-photo">
              <span class="poster-badge" id="posterBadge">Share Card</span>
              <img id="mainImage" alt="Agent selfie image">
              <div class="photo-placeholder" id="photoPlaceholder">等待附加图片</div>
            </section>

            <section class="poster-body">
              <div class="identity-row">
                <h1 class="name" id="name">Agent Name</h1>
                <div class="role" id="role">Share Card Preview</div>
              </div>

              <p class="tagline" id="tagline">这是首页预览空态，不代表任何真实 agent 内容。</p>

              <div class="persona-tags" id="personaTags">
                <span class="persona-tag">结构预览</span>
                <span class="persona-tag">字段映射</span>
                <span class="persona-tag">待附加图片</span>
              </div>

              <div class="share-caption" id="shareCaption">载入正式数据后，这里会显示最终对外摘要。</div>

              <div class="poster-footer">
                <div class="owner" id="ownerBlock">
                  <p class="owner-line">主人 @owner</p>
                </div>
                <div class="poster-mark" id="posterMark">Shareable Agent Poster</div>
              </div>
            </section>
          </article>
        </div>
      </div>

      <div class="side">
        <section class="panel">
          <h2 class="headline" id="sideHeadline">Agent Share Card Preview</h2>
          <p class="subheadline" id="subheadline">首页默认展示的是空态预览。载入真实的 share-card 数据和最终图片后，这里才会显示正式内容。</p>
          <div class="meta-row" id="metaKeywords">
            <span class="meta-pill">memory-first</span>
            <span class="meta-pill">agent poster</span>
            <span class="meta-pill">shareable identity</span>
          </div>
        </section>

        <section class="panel">
          <div class="mini-grid">
            <div class="mini-card">
              <div class="mini-label">Best For</div>
              <div class="mini-value" id="bestFor">适合先读取真实记忆，再决定公开定位的 agent。</div>
            </div>
            <div class="mini-card">
              <div class="mini-label">Why It Spreads</div>
              <div class="mini-value" id="spreadReason">一眼看懂它真实稳定做什么，而不是看到旧案例拼接。</div>
            </div>
          </div>

          <div class="proof" style="margin-top: 14px;">
            <div class="proof-label">Why This Fits</div>
            <div class="proof-text" id="proofAnchor">基于真实 evidence 提炼，而不是沿用旧案例描述。</div>
          </div>

          <div class="mini-card" style="margin-top: 14px;">
            <div class="mini-label">Memory Basis</div>
            <div class="mini-value" id="memoryBasis">Built from real evidence when provided.</div>
          </div>

          <div class="prompt-card" style="margin-top: 14px;">
            <div class="mini-label">Selfie Prompt Preview</div>
            <pre id="selfiePrompt">Agent selfie prompt preview</pre>
          </div>

        </section>
      </div>
    </section>
  </main>

  <!-- CARD_DATA -->
  <script>
    const defaultData = {
      schema_version: "share-card/1.0",
      card_id: "card:placeholder",
      name: "Agent Name",
      role: "Share Card Preview",
      tagline: "这是首页预览空态，不代表任何真实 agent 内容。",
      value_line: "用于检查卡片结构、字段映射和图片附加效果。",
      top_capabilities: ["结构预览", "字段映射", "待附加图片"],
      best_for: "在接入真实 share-card.json 之前检查首页展示状态。",
      proof_anchor: "当前页面使用的是内置空态数据，不是实际 submission 结果。",
      persona_tags: ["结构预览", "字段映射", "待附加图片"],
      share_caption: "载入正式数据后，这里会显示最终对外摘要。",
      spread_line: "Share Card Preview：当前展示的是空态预览，不是正式卡片。",
      memory_basis: "No live card data loaded yet.",
      owner: { name: "Preview Mode" },
      image: null,
      qr: null,
      theme: {
        background: "#FFF7DF",
        border: "#111111",
        accent: "#FF7548",
        tag_colors: ["#FFD865", "#FFB78A", "#9DE2D3"]
      },
      selfie_prompt: "A validated 8-bit image prompt will appear here after real card data is loaded.",
      i18n: {
        zh: {
          role: "仅供预览",
          tagline: "这是首页预览空态，不代表任何真实 agent 内容。",
          value_line: "用于检查卡片结构、字段映射和图片附加效果。",
          top_capabilities: ["结构预览", "字段映射", "待附加图片"],
          best_for: "在接入真实 share-card.json 之前检查首页展示状态。",
          proof_anchor: "当前页面使用的是内置 fixture 数据，不是实际 submission 结果。",
          spread_line: "仅供预览：当前展示的是空态预览，不是正式卡片。",
          memory_basis: "当前尚未载入真实卡片数据。",
          selfie_prompt: "载入真实卡片数据后，这里会显示校验后的 8-bit 图片提示词。",
          persona_tags: ["结构预览", "字段映射", "待附加图片"],
          share_caption: "载入正式数据后，这里会显示最终对外摘要。",
          share_keywords: ["preview", "empty-state", "image-pending"]
        },
        en: {
          role: "Fixture Preview Only",
          tagline: "This is an empty preview state, not real agent content.",
          value_line: "Use it to check layout, field mapping, and image attachment behavior.",
          top_capabilities: ["Layout Preview", "Field Mapping", "Image Pending"],
          best_for: "Checking the page before loading a real share-card.json payload.",
          proof_anchor: "The page is using built-in fixture data, not an actual submission result.",
          spread_line: "Fixture preview only: this page is showing an empty preview state.",
          memory_basis: "No live card data loaded yet.",
          selfie_prompt: "A validated 8-bit image prompt will appear here after real card data is loaded.",
          persona_tags: ["Layout Preview", "Field Mapping", "Image Pending"],
          share_caption: "The final public summary will appear here after real data is loaded.",
          share_keywords: ["preview", "empty-state", "image-pending"]
        }
      }
    };

    const uiText = {
      zh: {
        bestFor: "适用场景",
        whySpreads: "适合分享的原因",
        whyFits: "为什么像它",
        memoryBasis: "记忆来源",
        promptPreview: "自拍提示词预览",
        exportPng: "导出海报 PNG",
        deployedBy: "主人",
        qr: "二维码",
        photoPlaceholder: "等待附加图片",
        sideHeadline: "Agent 分享卡片预览",
        sideSubheadline: "首页默认展示的是空态预览。载入真实的 share-card 数据和最终图片后，这里才会显示正式内容。",
        posterMark: "Agent 分享卡片"
      },
      en: {
        bestFor: "Best For",
        whySpreads: "Why It Spreads",
        whyFits: "Why This Fits",
        memoryBasis: "Memory Basis",
        promptPreview: "Selfie Prompt Preview",
        exportPng: "Export Poster PNG",
        deployedBy: "DEPLOYED BY",
        qr: "QR",
        photoPlaceholder: "Image not attached",
        sideHeadline: "Agent Share Card Preview",
        sideSubheadline: "The home page starts in an empty preview state. Real content appears only after a valid share-card payload and final image are loaded.",
        posterMark: "Agent Share Card"
      }
    };

    let currentLanguage = document.documentElement.lang === "en" ? "en" : "zh";

    function readAsset(asset) {
      if (!asset) return null;
      if (typeof asset === "string") return asset;
      if (typeof asset === "object") return asset.data_url || asset.url || null;
      return null;
    }

    function normalizeShareCard(raw) {
      if (!raw || raw.schema_version !== "share-card/1.0") return defaultData;
      return {
        ...defaultData,
        ...raw,
        owner: { ...defaultData.owner, ...(raw.owner || {}) },
        top_capabilities: Array.isArray(raw.top_capabilities) ? raw.top_capabilities.slice(0, 3) : defaultData.top_capabilities,
        persona_tags: Array.isArray(raw.persona_tags) ? raw.persona_tags.slice(0, 3) : defaultData.persona_tags,
        theme: { ...defaultData.theme, ...(raw.theme || {}) },
        image: readAsset(raw.image),
        qr: readAsset(raw.qr),
        i18n: raw.i18n && typeof raw.i18n === "object" ? raw.i18n : null,
      };
    }

    function resolveLocalizedCard(card, lang) {
      const localized = card?.i18n?.[lang];
      if (!localized) return card;
      return {
        ...card,
        role: localized.role || card.role,
        tagline: localized.tagline || card.tagline,
        value_line: localized.value_line || card.value_line,
        top_capabilities: Array.isArray(localized.top_capabilities) ? localized.top_capabilities.slice(0, 3) : card.top_capabilities,
        best_for: localized.best_for || card.best_for,
        proof_anchor: localized.proof_anchor || card.proof_anchor,
        spread_line: localized.spread_line || card.spread_line,
        memory_basis: localized.memory_basis || card.memory_basis,
        selfie_prompt: localized.selfie_prompt || card.selfie_prompt,
        poster_headline: localized.poster_headline || card.poster_headline,
        poster_subline: localized.poster_subline || card.poster_subline,
        persona_tags: Array.isArray(localized.persona_tags) ? localized.persona_tags.slice(0, 3) : card.persona_tags,
        share_caption: localized.share_caption || card.share_caption,
        share_keywords: Array.isArray(localized.share_keywords) ? localized.share_keywords : card.share_keywords,
      };
    }

    function inferRoleBackdrop(role) {
      if (!role) return "";
      if (role.includes("销售")) return "pipeline";
      if (role.includes("产品") || role.includes("策略")) return "strategy";
      if (role.includes("招聘")) return "hiring";
      if (role.includes("支持") || role.includes("客服")) return "support";
      return "";
    }

    function inferPosterBadge(card) {
      const name = String(card?.name || "").trim();
      if (name.includes("ShrimpCard")) return "ShrimpCard";
      if (name) return name.length <= 18 ? name : "Share Card";
      return "Share Card";
    }

    function applyTheme(theme) {
      const root = document.documentElement;
      root.style.setProperty("--bg", theme.background || defaultData.theme.background);
      root.style.setProperty("--border", theme.border || defaultData.theme.border);
      root.style.setProperty("--accent", theme.accent || defaultData.theme.accent);
      const tagColors = Array.isArray(theme.tag_colors) && theme.tag_colors.length >= 3 ? theme.tag_colors : defaultData.theme.tag_colors;
      root.style.setProperty("--tag-1", tagColors[0]);
      root.style.setProperty("--tag-2", tagColors[1]);
      root.style.setProperty("--tag-3", tagColors[2]);
      root.style.setProperty("--accent-strong", tagColors[2]);
      root.style.setProperty("--accent-2", tagColors[2]);
    }

    function renderTags(tags) {
      const node = document.getElementById("personaTags");
      if (!node) return;
      const colors = ["var(--tag-1)", "var(--tag-2)", "var(--tag-3)"];
      node.replaceChildren(...tags.map((tag, index) => {
        const span = document.createElement("span");
        span.className = "persona-tag";
        span.style.background = colors[index] || "#fff";
        span.textContent = tag;
        return span;
      }));
    }

    function renderOwner(owner, lang) {
      const node = document.getElementById("ownerBlock");
      if (!node) return;
      const lines = [];
      if (owner.name) lines.push(`${uiText[lang].deployedBy} ${owner.name}`);
      if (owner.contact) lines.push(owner.contact);
      node.replaceChildren(...lines.map((line) => {
        const div = document.createElement("div");
        div.textContent = line;
        return div;
      }));
    }

    function renderImage(src, imageId, placeholderId) {
      const img = document.getElementById(imageId);
      const placeholder = document.getElementById(placeholderId);
      const photo = img ? img.closest(".poster-photo") : null;
      if (!img || !placeholder) return;
      if (src) {
        img.src = src;
        img.style.display = "block";
        placeholder.style.display = "none";
        if (photo) photo.dataset.empty = "false";
      } else {
        img.removeAttribute("src");
        img.style.display = "none";
        placeholder.style.display = "flex";
        if (photo) photo.dataset.empty = "true";
      }
    }

    function applyImageStrategy(card) {
      const photo = document.querySelector(".poster-photo");
      if (!photo) return;
      photo.dataset.guideMode = card.image ? "false" : "true";
      const roleBackdrop = card.role_backdrop || "";
      photo.dataset.roleBackdrop = roleBackdrop;
      const exportCard = document.getElementById("exportCard");
      if (exportCard) {
        exportCard.dataset.roleBackdrop = roleBackdrop;
        exportCard.dataset.guideMode = card.image ? "false" : "true";
      }
    }

    function renderKeywords(card) {
      const keywords = (Array.isArray(card.share_keywords) ? card.share_keywords : []).slice(0, 4);
      const node = document.getElementById("metaKeywords");
      if (!node) return;
      node.replaceChildren(...keywords.map((keyword) => {
        const span = document.createElement("span");
        span.className = "meta-pill";
        span.textContent = keyword;
        return span;
      }));
    }

    function applyUiLanguage(lang) {
      const ui = uiText[lang] || uiText.zh;
      const labels = document.querySelectorAll(".mini-label");
      if (labels[0]) labels[0].textContent = ui.bestFor;
      if (labels[1]) labels[1].textContent = ui.whySpreads;
      if (labels[2]) labels[2].textContent = ui.memoryBasis;
      if (labels[3]) labels[3].textContent = ui.promptPreview;
      const proofLabel = document.querySelector(".proof-label");
      if (proofLabel) proofLabel.textContent = ui.whyFits;
      const qrPlaceholder = document.getElementById("qrPlaceholder");
      if (qrPlaceholder) qrPlaceholder.textContent = ui.qr;
      const photoPlaceholder = document.getElementById("photoPlaceholder");
      if (photoPlaceholder) photoPlaceholder.textContent = ui.photoPlaceholder;
      const posterMark = document.getElementById("posterMark");
      if (posterMark) posterMark.textContent = ui.posterMark;
    }

    function renderCard(rawData, lang = currentLanguage) {
      const ui = uiText[lang] || uiText.zh;
      const baseCard = normalizeShareCard(rawData);
      const card = resolveLocalizedCard(baseCard, lang);
      const roleBackdrop = baseCard.role_backdrop || inferRoleBackdrop(baseCard.role);
      card.role_backdrop = roleBackdrop;
      card.image = baseCard.image;
      applyTheme(card.theme || {});
      applyUiLanguage(lang);

      const posterBadge = document.getElementById("posterBadge");
      if (posterBadge) posterBadge.textContent = inferPosterBadge(card);
      const sideHeadline = document.getElementById("sideHeadline");
      if (sideHeadline) sideHeadline.textContent = card.poster_headline || card.role || ui.sideHeadline;
      const subheadline = document.getElementById("subheadline");
      if (subheadline) subheadline.textContent = card.poster_subline || card.value_line || card.tagline || ui.sideSubheadline;
      const name = document.getElementById("name");
      if (name) name.textContent = card.name;
      const role = document.getElementById("role");
      if (role) role.textContent = card.role;
      const tagline = document.getElementById("tagline");
      if (tagline) tagline.textContent = card.tagline;
      const shareCaption = document.getElementById("shareCaption");
      if (shareCaption) shareCaption.textContent = card.share_caption || card.spread_line;
      const bestFor = document.getElementById("bestFor");
      if (bestFor) bestFor.textContent = card.best_for;
      const spreadReason = document.getElementById("spreadReason");
      if (spreadReason) spreadReason.textContent = card.spread_line;
      const proofAnchor = document.getElementById("proofAnchor");
      if (proofAnchor) proofAnchor.textContent = card.proof_anchor;
      const memoryBasis = document.getElementById("memoryBasis");
      if (memoryBasis) memoryBasis.textContent = card.memory_basis;
      const selfiePrompt = document.getElementById("selfiePrompt");
      if (selfiePrompt) selfiePrompt.textContent = card.selfie_prompt || defaultData.selfie_prompt;
      renderTags(card.persona_tags && card.persona_tags.length ? card.persona_tags : defaultData.persona_tags);
      renderOwner(card.owner || {}, lang);
      renderKeywords(card);
      applyImageStrategy(card);
      renderImage(card.image, "mainImage", "photoPlaceholder");
    }

    const cardSource = window.__CARD_DATA__ || defaultData;
    renderCard(cardSource, currentLanguage);
  </script>
</body>
</html>


path: agents/openai.yaml
content: interface:
  display_name: "OpenClaw ShrimpCard"
  short_description: "Turn agent evidence into a validated share card and final HTML selfie card."
  default_prompt: "Use openclaw-shrimpcard to derive an evidence-backed self-intro, validate the JSON, attach a real 8-bit character image, and render the final HTML card."


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)