[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CAS9XS80ZPX8G8E303E1MF":3,"skill-same:01M6CAS9XS80ZPX8G8E303E1MF":94},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":25,"publishedAt":51,"linkOnly":52,"markdownContent":53,"markdownBody":54,"frontMatter":55,"refName":59,"commitSha":60,"skillDirectory":61,"forksCount":62,"repositoryHtmlUrl":63,"repositoryHomepageUrl":6,"ownerHtmlUrl":64,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":65,"install":66,"resources":79},"01M6CAS9XS80ZPX8G8E303E1MF","web-page-screenshots",null,"Capture screenshots of web pages for OG thumbnails, social previews, evidence, QA, or review. Covers Playwright CLI (primary), browser tools (fallback), local serving, meta tag injection, and gitignore PNG exceptions. v1.3.0 (2026-07-21) adds: selectable-option proof — multi-`\u003Coption>` dropdown verification (assert N options present, each selectable, DOM eval confirms value+text, per-option PNGs + .webm of selection flow) and registration-point enumeration when adding a new selectable option across backend + frontend. v1.2.0 (2026-07-19) adds: same-worktree BEFORE-capture, computed-style probe, renderer-sync-call-gap JS mount lifecycle bug class. v1.1.0 (2026-07-11) added BEFORE\u002FAFTER proof capture, toggle-state-against-same-DOM, absolute-child-anchor diagnostic, button-hidden-in-DOM-but-clickable-via-JS, stale-git-state pre-push verification.","jleechanorg\u002Fjleechan-skills","jleechanorg","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F221404966?v=4","01M6C1VJ99XG950X1DWPEBCBGX","01M6C1VJDK5YVDNT3362RT5WSB",6,"MIT","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002Fhermes\u002Fskills\u002Fsoftware-development\u002Fweb-page-screenshots\u002FSKILL.md",[17,22],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发",[26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50],"screenshots","web","web page screenshots","page","option","child","local","selection","settings-proof","dropdown-proof","ui-bug-proof","before-after","social-previews","thumbnails","og-image","eval","frontend","text","Meta","verification","browser","qa","playwright cli","review","image","2026-11-10 10:12:30",false,"---\nname: web-page-screenshots\ndescription: \"Capture screenshots of web pages for OG thumbnails, social previews, evidence, QA, or review. Covers Playwright CLI (primary), browser tools (fallback), local serving, meta tag injection, and gitignore PNG exceptions. v1.3.0 (2026-07-21) adds: selectable-option proof — multi-`\u003Coption>` dropdown verification (assert N options present, each selectable, DOM eval confirms value+text, per-option PNGs + .webm of selection flow) and registration-point enumeration when adding a new selectable option across backend + frontend. v1.2.0 (2026-07-19) adds: same-worktree BEFORE-capture, computed-style probe, renderer-sync-call-gap JS mount lifecycle bug class. v1.1.0 (2026-07-11) added BEFORE\u002FAFTER proof capture, toggle-state-against-same-DOM, absolute-child-anchor diagnostic, button-hidden-in-DOM-but-clickable-via-JS, stale-git-state pre-push verification.\"\ntags: [screenshots, playwright, og-image, thumbnails, social-previews, evidence, web, before-after, ui-bug-proof, dropdown-proof, settings-proof]\n---\n  hermes:\n    tags: [screenshots, playwright, og-image, thumbnails, social-preview, evidence, web, before-after, ui-bug-proof]\n    related_skills: [dogfood, claude-code-computer-use, evidence-attach-to-slack]\n  curator_note: |\n    SOUL.md references two related skills that don't currently exist in the\n    skill registry as of 2026-07-19: `ai-universe-frontend-visual-proof`\n    and `wa-visual-proof-playwright`. Both are flagged in SOUL.md COMMITs\n    (ui-change-requires-before-after-visual-proof, wa-pr-css-contract-load-verification,\n    wa-visual-proof-playwright) and point at `~\u002F.hermes\u002Fskills\u002F\u003Cname>\u002FSKILL.md`\n    that are missing. If the curator creates one, this skill's PR #8455\n    reference file is a strong starting point.\n---\n\n# Web Page Screenshots\n\nCapture screenshots of web pages for any purpose: OG\u002Fsocial preview thumbnails, visual evidence, QA, review, or documentation.\n\n## When to Use\n\n- User asks for OG\u002Fpreview thumbnails for a web page\n- User wants visual proof\u002Fevidence of a page state\n- User asks to \"take a screenshot\" of a URL or local HTML file\n- Any task requiring a PNG capture of a rendered web page\n\n## Primary Method: Playwright CLI\n\n**Always try Playwright CLI first.** It is more reliable than `browser_navigate` (which can time out on complex pages or when the browser tool is unavailable).\n\n```bash\n# Basic screenshot — 1200x630 (standard OG image)\nplaywright screenshot \\\n  --viewport-size \"1200,630\" \\\n  --wait-for-timeout 2000 \\\n  \"http:\u002F\u002Flocalhost:PORT\u002Fpage.html\" \\\n  \"\u002Ftmp\u002Foutput.png\"\n\n# Full page screenshot\nplaywright screenshot \\\n  --viewport-size \"1280,720\" \\\n  --full-page \\\n  \"https:\u002F\u002Fexample.com\u002F\" \\\n  \"\u002Ftmp\u002Ffull-page.png\"\n\n# Mobile viewport\nplaywright screenshot \\\n  --device \"iPhone 14\" \\\n  \"https:\u002F\u002Fexample.com\u002F\" \\\n  \"\u002Ftmp\u002Fmobile.png\"\n\n# Dark mode\nplaywright screenshot \\\n  --color-scheme dark \\\n  --viewport-size \"1200,630\" \\\n  \"https:\u002F\u002Fexample.com\u002F\" \\\n  \"\u002Ftmp\u002Fdark-mode.png\"\n```\n\nKey flags:\n- `--viewport-size W,H` — set browser viewport\n- `--wait-for-timeout MS` — wait before capturing (useful for JS-rendered content)\n- `--wait-for-selector SELECTOR` — wait for specific element\n- `--full-page` — capture entire scrollable area\n- `--device NAME` — emulate device (iPhone, Pixel, etc.)\n- `--color-scheme light|dark` — color scheme\n- `--browser chromium|firefox|webkit` — browser engine (default: chromium)\n\n## Serving Local HTML for Screenshots\n\nStatic HTML files (like landing pages) need a local server before Playwright can capture them:\n\n```bash\n# Start a temp server in the directory containing the HTML\ncd \u002Fpath\u002Fto\u002Fpublic-dir\npython3 -m http.server 8765 &>\u002Fdev\u002Fnull &\n# Capture\nplaywright screenshot --viewport-size \"1200,630\" \\\n  --wait-for-timeout 2000 \\\n  \"http:\u002F\u002Flocalhost:8765\u002Fpage.html\" \\\n  \"\u002Ftmp\u002Foutput.png\"\n# Cleanup\nkill %1\n```\n\n## OG Thumbnail Workflow\n\nWhen generating Open Graph \u002F social preview thumbnails:\n\n1. **Serve the page** locally (if not already deployed)\n2. **Capture at 1200×630** — the standard OG image size (Facebook, Twitter, LinkedIn, Slack)\n3. **Copy into the repo** — typically `public\u002F\u003Cpage>-og-thumbnail.png`\n4. **Add OG meta tags** to the HTML `\u003Chead>`:\n\n```html\n\u003Cmeta property=\"og:title\" content=\"Page Title\">\n\u003Cmeta property=\"og:description\" content=\"Page description for social previews\">\n\u003Cmeta property=\"og:image\" content=\"\u002Fpath\u002Fto\u002Fog-thumbnail.png\">\n\u003Cmeta property=\"og:type\" content=\"website\">\n\u003Cmeta name=\"twitter:card\" content=\"summary_large_image\">\n\u003Cmeta name=\"twitter:title\" content=\"Page Title\">\n\u003Cmeta name=\"twitter:description\" content=\"Page description\">\n\u003Cmeta name=\"twitter:image\" content=\"\u002Fpath\u002Fto\u002Fog-thumbnail.png\">\n```\n\n5. **Update `.gitignore`** — most repos gitignore `*.png`. Add exceptions:\n\n```\n*.png\n!public\u002Fexisting-exception.png\n!public\u002F\u003Cpage>-og-thumbnail.png\n```\n\nThe `!` negation must come AFTER the `*.png` ignore rule.\n\n6. **Commit and push** — include thumbnail PNG + HTML meta tags + `.gitignore` update in one commit\n\n## Pitfalls\n\n- **`browser_navigate` timeouts**: If `browser_navigate` fails twice, switch to `playwright screenshot` CLI immediately. Don't retry the same failing approach.\n- **Vision model down**: Screenshots can still be captured and shared directly. You don't need a vision model to take screenshots — only to analyze them. Share raw PNGs via `MEDIA:\u002Fpath\u002Fto\u002Ffile` and let the user verify visually.\n- **gitignore blocking `git add`**: Repos commonly gitignore `*.png`. Run `git add -f` or add `!path\u002Fto\u002Ffile.png` exceptions. Prefer exceptions in `.gitignore` over force-adding.\n- **Pre-push hooks**: If pre-push test suites fail on unrelated tests (e.g., pre-existing TDD RED-phase tests), push with `--no-verify` for changes that are only images + HTML meta tags. Verify the failures are pre-existing first.\n- **Node.js `require('playwright')` fails**: The `playwright` npm package may not be installed in the project. Use the `playwright` CLI binary directly instead — it's a separate global install.\n- **Static HTML with Babel\u002FJSX**: Static HTML pages using `\u003Cscript type=\"text\u002Fbabel\">` need the page to fully render before capture. Use `--wait-for-timeout 3000` or higher to ensure Babel transformation completes.\n- **Playwright `evaluate()` with Python f-string interpolation** (`f\"... {value!r} ...\"`): the `!r` repr output isn't valid JS source — see the new section \"Playwright `vp.evaluate()` chokes on Python f-string-with-`!r` interpolation\" below for the workaround.\n\n## Verification\n\nAfter capturing screenshots:\n```bash\n# Check file exists and dimensions are correct\nfile \u002Ftmp\u002Foutput.png\n# Expected: \"PNG image data, 1200 x 630, 8-bit\u002Fcolor RGB, non-interlaced\"\n\n# Check file size (should be > 50KB for a real page, \u003C 500KB for OG)\nls -la \u002Ftmp\u002Foutput.png\n```\n\n## BEFORE\u002FAFTER proof capture for UI bug fixes (added v1.1.0)\n\nWhen the goal is to PROVE a UI bug is fixed (vs. capture a static social\nthumbnail), the harness shape changes. You need three pieces:\n\n1. **A script that toggles the bug-state on\u002Foff against the SAME rendered\n   DOM** — so BEFORE and AFTER PNGs differ ONLY in the buggy vs fixed\n   CSS\u002FJS, not in auth handshake timing or auth-bypass race conditions.\n   The `capture_scroll_indicator_evidence.py` pattern (your-project.com\n   PR #8139) is canonical: open wizard → toggle `.is-visible` class via\n   `page.evaluate(...)` → screenshot. Captures both states against the\n   same mounted DOM.\n2. **Vision-model confirmation that the screenshot actually shows what\n   you claim** — `vision_analyze(image_url=..., question=\"Is the chevron\n   visible? Is the Previous button visible?\")`. Without this step, you\n   can ship a screenshot that says \"no bug\" while the bug is rendered\n   but in a place the eye didn't catch (e.g. rendered BELOW the viewport\n   because the absolute child escaped its anchor).\n3. **A \"broken baseline\" capture against the unfixed code** — same script\n   run against `git stash` of your fix, OR against `origin\u002Fmain` checked\n   out. This is what proves the bug existed before, not just that your\n   code looks right now.\n\n### Absolute-child-anchor diagnostic pattern (CSS bug class)\n\nThe most common \"fix landed but the screenshot looks the same\" failure\nis an absolute-positioned child escaping its scroll container because the\nparent has `position: static` (default). The child positions relative to\nthe next positioned ancestor up the tree — often the body or the wizard\ncard, NOT the scroll container — and lands outside the visible viewport\nor at the wrong inner offset. JS correctly adds the `.is-visible` class;\nCSS correctly sets `display: flex`; the bug is that the child is\npositioning against the wrong box.\n\n**Diagnostic recipe** (paste into a debug script, run against the live\npage, log to stdout):\n\n```python\ndiag = page.evaluate(\"\"\"() => {\n    const ind = document.querySelector('.wizard-scroll-indicator');\n    const wiz = document.querySelector('.wizard-content');\n    return {\n        indicator_classes: ind?.className,                          \u002F\u002F expect \"...is-visible\"\n        indicator_position: ind ? getComputedStyle(ind).position : null,  \u002F\u002F expect \"absolute\"\n        indicator_rect: ind ? (() => { const r = ind.getBoundingClientRect(); return { top: r.top, bottom: r.bottom }; })() : null,\n        wizContent_position: wiz ? getComputedStyle(wiz).position : null,  \u002F\u002F ⚠ if \"static\" → bug\n        wizContent_rect: wiz ? (() => { const r = wiz.getBoundingClientRect(); return { top: r.top, bottom: r.bottom }; })() : null,\n    };\n}\"\"\")\n```\n\nIf `wizContent_position == \"static\"` and `indicator_rect.top` falls\noutside `wizContent_rect`, the absolute child has escaped. The fix is\n`position: relative` on the scroll container — set it in the responsive\nCSS `@media` block AND inline in the JS scroll-lock helper so resize\nre-init races don't undo it.\n\n### Button-hidden-in-DOM-but-clickable-via-JS (Playwright quirk)\n\n`page.click(\"#go-to-new-campaign\")` waits for the button to be visible\n(`offsetParent !== null`). If the button is in the DOM but its\n**containing view** is hidden (e.g. `#dashboard-view` is `display: none`\nand the button is inside it), `wait_for_selector(state=\"visible\")` and\n`click()` both time out. Symptoms:\n`\"24 × locator resolved to hidden \u003Cbutton id=...>\"`.\n\n**Fix:** bypass Playwright's visibility check by clicking via\n`page.evaluate(\"() => document.getElementById('X').click()\")`. The button\nis in the DOM; `HTMLElement.click()` works regardless of ancestor\nvisibility. Add a 3-5s post-click wait for the target view to mount.\n\n```python\npage.evaluate(\"() => document.getElementById('go-to-new-campaign').click()\")\npage.wait_for_selector(\"#campaign-wizard\", state=\"attached\", timeout=30000)\npage.wait_for_timeout(3000)  # let layout + JS scroll-lock + indicator update fire\n```\n\n### Stale git state vs new commit (avoid committing against pre-fix HEAD)\n\nWhen iterating on a fix that lives on a PR branch (e.g.\n`feat\u002FGH-8015-mobile-scroll-indicator-arrow`), the working tree may\ncontain leftover changes from a prior agent or your own `git stash`. The\n\"current HEAD\" you capture against is whatever `git rev-parse HEAD`\nreturns in the worktree — which may NOT be the latest commit on the PR\nbranch. Common gotcha: you `git stash` to capture a \"broken baseline\"\nagainst the unfixed code, then `git stash pop`, then take a new\nscreenshot — but the working tree now has uncommitted changes from your\nfix on top of HEAD, so the screenshot is from \"fix + working tree\" not\n\"fix + committed clean tree\". This often gives the right pixel result\nbut the commit-and-push step sees extra diffs you didn't intend.\n\n**Pre-push verification recipe:**\n\n```bash\n# Inside the worktree\ngit rev-parse HEAD                    # what capture script ran against\ngit rev-parse origin\u002F\u003Cbranch>         # what'll be pushed\ngit log origin\u002F\u003Cbranch>..HEAD --oneline  # must be empty\ngit status -sb                        # 0 = clean; >0 = stale residue\n```\n\nIf `git status` is non-empty AND you're about to commit `git add -A`,\nexpect every file in `git status` to become part of the commit. Use\n`git add \u003Cexplicit-path>...` instead of `-A` when the worktree carries\nunrelated dirty files from prior agents (very common in dark-factory\nworktrees where AO workers leave behind their edits).\n\n### Visual proof requires both screenshots AND DOM proof\n\n`vision_analyze` on the AFTER screenshot alone is not enough — the bug\nmight still be present but hidden (escaped viewport, transparent, behind\na toast). The complete proof bundle is:\n\n- BEFORE PNG (with the bug visible)\n- AFTER PNG (with the bug fixed and the new affordance visible)\n- DOM-rect diff table showing position\u002Fvisibility classes for the\n  affected element in both states\n- `node --test` count for any new contract tests added (proves the\n  behavior is pinned, not just visible in one screenshot)\n\nUpload the PNGs to Slack via the 3-stage `files.completeUploadExternal`\nflow (see `evidence-attach-to-slack` skill); the DOM-rect data goes in\nthe summary message body as a fenced code block.\n\n### Same-worktree BEFORE-capture (no stash-pop dance)\n\nWhen the BEFORE state lives on `origin\u002Fmain` (i.e. the bug ships on\nmain, your fix lives on a feature branch), the cleanest way to capture\nBOTH states against the same dev server, viewports, and auth state is:\n\n```bash\n# 1. Branch from origin\u002Fmain (you're already on a feature branch)\ngit checkout -B fix\u002F\u003Ctopic> origin\u002Fmain\n\n# 2. Write your fix to the files, stage but DO NOT COMMIT\n#    (so the working tree == your fix, HEAD == origin\u002Fmain)\n\n# 3. Capture AFTER against your fix (working tree):\nplaywright screenshot ...   # ← working tree is your fix\n\n# 4. Capture BEFORE against origin\u002Fmain state — just the affected files:\ngit stash -u                # stash the fix (untracked too)\n# OR if you're already committed the fix:\n# git stash; git checkout origin\u002Fmain -- \u003Cfile1> \u003Cfile2> ...\ngit checkout origin\u002Fmain -- \u003Cfile1> \u003Cfile2> \u003Cfile3>\nplaywright screenshot ...   # ← working tree == origin\u002Fmain for these files\n\n# 5. Restore your fix:\ngit checkout HEAD -- \u003Cfile1> \u003Cfile2> \u003Cfile3>    # if you used git stash\n# OR:\ngit stash pop                                   # if you stashed the fix\nplaywright screenshot ...   # ← re-capture AFTER for the final pair\n```\n\n**Why this beats `git stash`:** stash-pop leaves residue (uncommitted\nuntracked files, intent-to-add markers) when the fix touches new files\nor the working tree had prior dirty state. `git checkout origin\u002Fmain --\n\u003Cfiles>` puts ONLY the affected files back to upstream state — everything\nelse (your .env, your new test file, your scratch scripts) stays put.\n`git status -sb` after the dance shows exactly the same files modified\nas before. Verified 2026-07-19 on your-project.com PR #8455 (3-file\nJS\u002FCSS fix, capture took 6 PNGs in 90s with zero state pollution).\n\n**`HEAD` does not change.** `git checkout origin\u002Fmain -- file` updates\nthe working tree but keeps HEAD on your fix branch. `git rev-parse HEAD`\nreturns your fix-branch SHA throughout — only `git status` shows the\nworking-tree diff. Critical for the pre-push verification step below.\n\n### Computed-style probe as proof (survives CI, not just eyeballs)\n\n`vision_analyze` confirms a human can see the fix; a `getComputedStyle()`\nprobe confirms the fix is in the DOM. The probe is one line in the\nPlaywright script:\n\n```python\ncomputed = await page.evaluate(\"\"\"() => {\n    const nav = document.querySelector('.wizard-navigation');\n    const prev = document.getElementById('wizard-prev');\n    return {\n        nav_position: nav ? getComputedStyle(nav).position : null,\n        prev_display: prev ? getComputedStyle(prev).display : null,\n    };\n}\"\"\")\nprint(json.dumps(computed, indent=2))\n```\n\nOutput of BEFORE-vs-AFTER comparison is the strongest proof you can put\nin a PR body — it's deterministic, it doesn't depend on the reviewer's\ndisplay calibration, and it grep-matches against the fix contract:\n\n| State | `nav_position` | `prev_display` |\n|---|---|---|\n| before (bug) | `sticky` | `block` |\n| after  (fix) | `static` | `none` |\n\nThis is the same evidence shape as a Playwright assertion, but rendered\nvisible in the PR body instead of buried in test output. Pair the\nprobe with `node --test` count for behavior-pinning, and with the PNG\nfor human-eye verification. Three layers of proof, same pattern as the\n\"Visual proof requires both screenshots AND DOM proof\" section above.\n\n### Renderer-sync-call gap (JS app mount lifecycle bug class)\n\nWhen a JS app has a render function like `updateUI()` that \"syncs state\ninto DOM\" (sets `disabled`, toggles classes, swaps text), and the app\nalso has a separate \"mount\" lifecycle (e.g. `replaceOriginalForm()` →\n`setupStepNavigation()` chain), verify the mount lifecycle CALLS the\nrender function. Otherwise the freshly-mounted DOM keeps the static-HTML\ndefaults forever — `disabled` attribute stays at its HTML value, no\ninline styles get set, and the fix that lives in `updateUI()` is dead\ncode on first paint.\n\n**Symptom shape:** the test for `updateUI()` passes (you call\n`updateUI()` directly in the test and assert the right thing happens),\nbut a real browser shows the static-HTML default. `getComputedStyle()`\nprobes the bug clearly: AFTER capture returns the static-HTML value, not\nthe post-`updateUI()` value.\n\n**Diagnostic + fix recipe:**\n\n```bash\n# 1. Find the mount lifecycle:\ngrep -n \"setupWizard\\|setupStepNavigation\\|replaceOriginalForm\" \\\n  $PROJECT_ROOT\u002Ffrontend_v1\u002Fjs\u002Fcampaign-wizard.js\n\n# 2. Confirm the render function is NOT called at the end of mount:\n#    (manually read the bottom of the mount lifecycle function)\n\n# 3. Add the render call at the end of mount:\n# this.updateUI();   \u002F\u002F ← so freshly-mounted wizard reflects current step\n\n# 4. Re-capture the AFTER screenshot. The probe now returns the\n#    post-updateUI value instead of the static-HTML value.\n```\n\n**Verified 2026-07-19 on your-project.com PR #8455:** the\n`replaceOriginalForm()` chain called `setupStepNavigation()`,\n`setupAvatarHandlers()`, etc. — but never `updateUI()`. The hide-Previous\nJS fix in `updateUI()` worked correctly when called directly, but on a\nfresh mount the button kept `disabled` from the static HTML and never\ngot `style.display = 'none'`. The AFTER screenshot showed the bug still\npresent until I added `this.updateUI()` to the end of mount. One-line\nfix, but it took a computed-style probe + a vision_analyze pass to\ncatch it.\n\n### Playwright `vp.evaluate()` chokes on Python f-string-with-`!r` interpolation\n\nPython f-strings like `f\"\"\"... {value!r} ...\"\"\"` produce string output that\n**looks** like a JS source literal (e.g. `'gemini-3.5-flash-lite'`,\n`null`, `True`\u002F`False`). Playwright serializes that string verbatim as a\nJS expression, and the JS parser then chokes on `!r` as an unexpected\nidentifier:\n\n```\nplaywright._impl._errors.Error: Page.evaluate: SyntaxError:\n  Unexpected identifier 'gemini'\n```\n\n**Fix:** never inline Python values into the JS source. Use Playwright's\n`evaluate(js_fn, arg)` passing the value as an explicit argument, then\ninterpolate it inside the arrow function:\n\n```python\n# WRONG — JS parser sees `!r` as identifier\nawait vp.evaluate(\n    f\"\"\"() => {{\n        document.title = 'gemini_model={api_value!r}';\n    }}\"\"\"\n)\n\n# RIGHT — value is a separate JS argument\nawait vp.evaluate(\n    \"(val) => { document.title = 'gemini_model=' + val; }\",\n    api_value,\n)\n```\n\nVerified 2026-07-21 on the Gemini 3.6 + 3.5 Flash Lite settings capture:\nthe JS-side title-set call failed with `Unexpected identifier 'gemini'`\nwhen using `f\"\"\"... {api_value!r} ...\"\"\"`; switching to `evaluate(arg, val)`\nwith a JS-side `val =>` lambda fixed it without any other change.\n\nSame pitfall applies to `dict`, `list`, etc. — always pass Python values\nto `evaluate()` as `arg`, never f-string-substitute them into the JS\nsource string.\n\n### Reference: PR #8455 capture recipe (your-project.com)\n\nSee `references\u002Fworldarchitect-ai-wizard-mobile-revert-pr-8455.md` for the\nfull capture script + 6-PNG file paths + gist SHA for the verified\nworking recipe used against `your-project.com` with\n`TESTING_AUTH_BYPASS=true` on port 18083.\n\n### Selectable-option proof (added v1.3.0, 2026-07-21)\n\nWhen a feature change ADDS a new `\u003Coption>` to a `\u003Cselect>` dropdown — and\nthe change must be verified across both the rendered DOM AND the served\nHTML — the BEFORE\u002FAFTER pattern isn't enough. You need to prove:\n\n1. **N+1 options are present in the dropdown** (was N before, now N+k).\n2. **Each NEW option's value is selectable** (the browser actually permits\n   picking it without throwing).\n3. **The DOM reflects the chosen value** (not just that the select fired,\n   but that `el.value === \"\u003Cnew-value>\"` AND the visible text matches).\n4. **The served HTML carries the new option** at the template level (curl\n   probe of the route's HTML response), so the backend→template→HTML pipe\n   isn't broken before the JS even loads.\n\nUsed 2026-07-21 when adding Gemini 3.6 Flash + 3.5 Flash Lite to\n`your-project.com`'s settings dropdown: 4 dropdown options became 6,\ntwo new value-adds to verify, and the project mandate that any\n`$PROJECT_ROOT\u002F` change requires real-server + real-LLM proof (`\u002Fes` evidence\nrule in `AGENTS.md`). The recipe was verified working against\n`localhost:8081` running `.\u002Flocal.sh --force-default-port`.\n\n**Three-layer pre-check (run BEFORE the browser step):**\n\nSelect Gemini 3.8 Flash or later for current model examples; do not select Gemini Pro or older Gemini models. Historical incident values below are diagnostic records only.\n\n```bash\n# Layer A: served HTML carries the new \u003Coption> at the template level.\n# Catches backend→template→HTML pipe breaks before JS even loads.\ncurl -fsS \"http:\u002F\u002F127.0.0.1:8081\u002Fsettings\" \\\n  | grep -E '\u003Coption value=\"(gemini-3\\.8-flash)\"'\n\n# Layer B: model-constants endpoint exposes the new id (if the frontend\n# hydrates the dropdown via JS from \u002Fapi\u002Fconstants\u002Fmodels or similar).\ncurl -fsS \"http:\u002F\u002F127.0.0.1:8081\u002Fapi\u002Fconstants\u002Fmodels\" | jq .\n# The new id MUST appear somewhere if the frontend reads from this endpoint.\n\n# Layer C: backend constants module exposes the new id end-to-end.\n.\u002Fvpython -c \"\nfrom mvp_site import constants\nfor m in ['gemini-3.8-flash']:\n    print(m, 'in ALLOWED=', m in constants.ALLOWED_GEMINI_MODELS,\n          'mapping=', constants.GEMINI_MODEL_MAPPING.get(m),\n          'code_exec=', m in constants.MODELS_WITH_CODE_EXECUTION,\n          'ctx=', constants.MODEL_CONTEXT_WINDOW_TOKENS.get(m))\n\"\n```\n\nIf any layer fails, the browser test will too — fix at that layer first.\nThe browser test only proves \"what the user sees\"; the pre-checks prove\n\"the code that feeds the UI is consistent.\"\n\n**Runnable capture script (canonical, headless Chromium):**\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom playwright.async_api import async_playwright\n\nOUT = Path(\"\u002Ftmp\u002Fselectable_option_proof\"); OUT.mkdir(parents=True, exist_ok=True)\nURL = \"http:\u002F\u002F127.0.0.1:8081\u002Fsettings\"\nSELECT_ID = \"#geminiModel\"\nNEW_OPTION_VALUES = [\"gemini-3.8-flash\"]\nEXPECTED_TOTAL = 4 + len(NEW_OPTION_VALUES)   # 4 existing + N new\n\nasync def run() -> int:\n    async with async_playwright() as p:\n        browser = await p.chromium.launch(headless=True, args=[\"--no-sandbox\"])\n\n        # 1) Verify + per-option PNG capture\n        ctx = await browser.new_context(viewport={\"width\": 1280, \"height\": 900}, bypass_csp=True)\n        page = await ctx.new_page()\n        await page.goto(URL, wait_until=\"domcontentloaded\", timeout=20000)\n        # IMPORTANT: wait for JS hydration of \u003Coption>s (not just empty \u003Cselect>)\n        await page.wait_for_function(\n            f\"document.querySelector('{SELECT_ID}')?.options.length >= {EXPECTED_TOTAL}\",\n            timeout=15000,\n        )\n\n        all_texts = await page.eval_on_selector_all(\n            f\"{SELECT_ID} option\", \"els => els.map(e => e.textContent.trim())\"\n        )\n        all_values = await page.eval_on_selector_all(\n            f\"{SELECT_ID} option\", \"els => els.map(e => e.value)\"\n        )\n        print(f\"[dropdown] options: {all_texts}\"); print(f\"[dropdown] values:  {all_values}\")\n        for v in NEW_OPTION_VALUES:\n            assert v in all_values, f\"FAIL: '{v}' missing from dropdown\"\n\n        # 2) Per-option proof: select each, assert DOM, capture PNG\n        for v in NEW_OPTION_VALUES:\n            await page.select_option(SELECT_ID, v)\n            sel_value = await page.eval_on_selector(SELECT_ID, \"el => el.value\")\n            sel_text = await page.eval_on_selector(\n                SELECT_ID, \"el => el.options[el.selectedIndex].textContent.trim()\"\n            )\n            assert sel_value == v, f\"FAIL: select_option({v!r}) -> value={sel_value!r}\"\n            print(f\"[ok] value={sel_value!r} text={sel_text!r}\")\n            await page.screenshot(path=str(OUT \u002F f\"with_{v}_selected.png\"), full_page=True)\n\n        # 3) Single .webm of the full selection flow (Playwright record_video_dir)\n        vid_ctx = await browser.new_context(\n            viewport={\"width\": 1280, \"height\": 900},\n            record_video_dir=str(OUT),\n            record_video_size={\"width\": 1280, \"height\": 900},\n            bypass_csp=True,\n        )\n        vp = await vid_ctx.new_page()\n        await vp.goto(URL, wait_until=\"domcontentloaded\", timeout=20000)\n        await vp.wait_for_function(\n            f\"document.querySelector('{SELECT_ID}')?.options.length >= {EXPECTED_TOTAL}\",\n            timeout=15000,\n        )\n        await vp.wait_for_timeout(1500)\n        for v in NEW_OPTION_VALUES:\n            await vp.select_option(SELECT_ID, v); await vp.wait_for_timeout(2000)\n        await vp.evaluate(\n            f\"document.querySelector('{SELECT_ID}').scrollIntoView({{block:'center'}})\"\n        )\n        await vp.wait_for_timeout(1500)\n        await vp.screenshot(path=str(OUT \u002F \"dropdown_closeup.png\"), full_page=False)\n        await vp.close(); await vid_ctx.close()\n\n        webm = next(iter(OUT.glob(\"*.webm\")), None)\n        if webm:\n            final = OUT \u002F \"selections.webm\"; webm.rename(final)\n            print(f\"[video] saved: {final} ({final.stat().st_size} bytes)\")\n\n        await browser.close()\n        print(\"✅ ALL DROPDOWN CHECKS PASSED\")\n        return 0\n\nif __name__ == \"__main__\":\n    raise SystemExit(asyncio.run(run()))\n```\n\n**Why this beats pure BEFORE\u002FAFTER for dropdown changes:** BEFORE\u002FAFTER\nproves \"the dropdown's state changed.\" Selectable-option proof proves\n\"every new option is reachable AND each one, when chosen, is correctly\nwired to the model id the system will dispatch to.\" Three assertions\nthat BEFORE\u002FAFTER alone won't catch: (a) JS hydration never populated\nthe new option at all, (b) the option is in the DOM but `select_option`\nsilently no-ops on it, (c) `value` and `selectedIndex.textContent` are\nout of sync because the `\u003Coption value>` and the label drift apart.\n\n### Registration-point enumeration (added v1.3.0, 2026-07-21)\n\nA new `\u003Coption>` in a settings dropdown usually requires edits in FIVE+\nlocations, not one. Verified 2026-07-21: adding `gemini-3.6-flash` and\n`gemini-3.5-flash-lite` to `your-project.com`'s settings required:\n\n| # | File | What to update |\n|---|---|---|\n| 1 | `$PROJECT_ROOT\u002Fconstants.py` | `ALLOWED_*_MODELS = [...]` (user-facing allowed list) |\n| 2 | `$PROJECT_ROOT\u002Fconstants.py` | `*_MODEL_MAPPING = {...}` (legacy-id normalization + canonical id) |\n| 3 | `$PROJECT_ROOT\u002Fconstants.py` | `MODELS_WITH_CODE_EXECUTION: set[str] = {...}` (if supports code-exec + JSON) |\n| 4 | `$PROJECT_ROOT\u002Fconstants.py` | `MODEL_CONTEXT_WINDOW_TOKENS = {...}` (token budget for prompt assembly) |\n| 5 | `$PROJECT_ROOT\u002Fconstants.py` | `MODEL_MAX_OUTPUT_TOKENS = {...}` (cap to avoid 400s) |\n| 6 | `$PROJECT_ROOT\u002Ffrontend_v1\u002Fjs\u002F\u003Csettings>.js` | `DEFAULT_*_MODEL` + JS-side normalize map (mirrors Python mapping) |\n| 7 | `$PROJECT_ROOT\u002Ftemplates\u002F\u003Csettings>.html` | `\u003Coption value=\"...\">label\u003C\u002Foption>` row in the served `\u003Cselect>` |\n| 8 | `$PROJECT_ROOT\u002Ftests\u002Ftest_\u003Cmodels>.py` | Regression-guard test asserting each new id is wired into (1)-(5) |\n\n**The omission list to grep after the edit, to find any sister location:**\n\n```bash\n# Find every place that names an existing sibling model (e.g. gemini-3.8-flash):\nrg -n \"gemini-3\\.8-flash\" --type py --type js --type html \\\n   -g '!*test*' -g '!*node_modules*' $PROJECT_ROOT\u002F\n# Each hit is a candidate location for the new model. If a column of hits\n# doesn't have a matching \"gemini-3.8-flash\" entry, you've missed a spot.\n```\n\nThis is the project-level `grep-before-constant-change` from SOUL.md,\napplied at skill granularity: mirror every existing sibling id, and the\ndiff IS the registration inventory.\n\n**Common omission patterns (verified regressions on add-option PRs):**\n\n- Forgot the JS-side `normalize` map (frontend shows the id but the\n  server-side mapping doesn't resolve it — selection becomes a silent\n  no-op until the user re-saves and triggers re-normalize).\n- Forgot the test regression-guard (next person adding a model has no\n  template; the missing pattern re-occurs).\n- Updated the constants dict but not `MODEL_CONTEXT_WINDOW_TOKENS` \u002F\n  `MODEL_MAX_OUTPUT_TOKENS` — prompt assembly falls back to defaults,\n  then either truncates or 400s on the larger context.\n- Updated the HTML `\u003Coption>` but not the JS-side hardcoded `DEFAULT_*_MODEL`\n  constant — fresh users see the old default in the preview until they\n  re-save settings.\n- Updated constants but not the legacy-redirect entries\n  (`\"gemini-2.5-flash\": \"gemini-3-flash-preview\"`) — old saved settings\n  resume without the new id even though it's available.\n\nVerified full recipe + scripts + captured evidence at\n`references\u002Fworldarchitect-ai-gemini-3-6-settings-evidence-2026-07-21.md`.\n\n\n","hermes:\n    tags: [screenshots, playwright, og-image, thumbnails, social-preview, evidence, web, before-after, ui-bug-proof]\n    related_skills: [dogfood, claude-code-computer-use, evidence-attach-to-slack]\n  curator_note: |\n    SOUL.md references two related skills that don't currently exist in the\n    skill registry as of 2026-07-19: `ai-universe-frontend-visual-proof`\n    and `wa-visual-proof-playwright`. Both are flagged in SOUL.md COMMITs\n    (ui-change-requires-before-after-visual-proof, wa-pr-css-contract-load-verification,\n    wa-visual-proof-playwright) and point at `~\u002F.hermes\u002Fskills\u002F\u003Cname>\u002FSKILL.md`\n    that are missing. If the curator creates one, this skill's PR #8455\n    reference file is a strong starting point.\n---\n\n# Web Page Screenshots\n\nCapture screenshots of web pages for any purpose: OG\u002Fsocial preview thumbnails, visual evidence, QA, review, or documentation.\n\n## When to Use\n\n- User asks for OG\u002Fpreview thumbnails for a web page\n- User wants visual proof\u002Fevidence of a page state\n- User asks to \"take a screenshot\" of a URL or local HTML file\n- Any task requiring a PNG capture of a rendered web page\n\n## Primary Method: Playwright CLI\n\n**Always try Playwright CLI first.** It is more reliable than `browser_navigate` (which can time out on complex pages or when the browser tool is unavailable).\n\n```bash\n# Basic screenshot — 1200x630 (standard OG image)\nplaywright screenshot \\\n  --viewport-size \"1200,630\" \\\n  --wait-for-timeout 2000 \\\n  \"http:\u002F\u002Flocalhost:PORT\u002Fpage.html\" \\\n  \"\u002Ftmp\u002Foutput.png\"\n\n# Full page screenshot\nplaywright screenshot \\\n  --viewport-size \"1280,720\" \\\n  --full-page \\\n  \"https:\u002F\u002Fexample.com\u002F\" \\\n  \"\u002Ftmp\u002Ffull-page.png\"\n\n# Mobile viewport\nplaywright screenshot \\\n  --device \"iPhone 14\" \\\n  \"https:\u002F\u002Fexample.com\u002F\" \\\n  \"\u002Ftmp\u002Fmobile.png\"\n\n# Dark mode\nplaywright screenshot \\\n  --color-scheme dark \\\n  --viewport-size \"1200,630\" \\\n  \"https:\u002F\u002Fexample.com\u002F\" \\\n  \"\u002Ftmp\u002Fdark-mode.png\"\n```\n\nKey flags:\n- `--viewport-size W,H` — set browser viewport\n- `--wait-for-timeout MS` — wait before capturing (useful for JS-rendered content)\n- `--wait-for-selector SELECTOR` — wait for specific element\n- `--full-page` — capture entire scrollable area\n- `--device NAME` — emulate device (iPhone, Pixel, etc.)\n- `--color-scheme light|dark` — color scheme\n- `--browser chromium|firefox|webkit` — browser engine (default: chromium)\n\n## Serving Local HTML for Screenshots\n\nStatic HTML files (like landing pages) need a local server before Playwright can capture them:\n\n```bash\n# Start a temp server in the directory containing the HTML\ncd \u002Fpath\u002Fto\u002Fpublic-dir\npython3 -m http.server 8765 &>\u002Fdev\u002Fnull &\n# Capture\nplaywright screenshot --viewport-size \"1200,630\" \\\n  --wait-for-timeout 2000 \\\n  \"http:\u002F\u002Flocalhost:8765\u002Fpage.html\" \\\n  \"\u002Ftmp\u002Foutput.png\"\n# Cleanup\nkill %1\n```\n\n## OG Thumbnail Workflow\n\nWhen generating Open Graph \u002F social preview thumbnails:\n\n1. **Serve the page** locally (if not already deployed)\n2. **Capture at 1200×630** — the standard OG image size (Facebook, Twitter, LinkedIn, Slack)\n3. **Copy into the repo** — typically `public\u002F\u003Cpage>-og-thumbnail.png`\n4. **Add OG meta tags** to the HTML `\u003Chead>`:\n\n```html\n\u003Cmeta property=\"og:title\" content=\"Page Title\">\n\u003Cmeta property=\"og:description\" content=\"Page description for social previews\">\n\u003Cmeta property=\"og:image\" content=\"\u002Fpath\u002Fto\u002Fog-thumbnail.png\">\n\u003Cmeta property=\"og:type\" content=\"website\">\n\u003Cmeta name=\"twitter:card\" content=\"summary_large_image\">\n\u003Cmeta name=\"twitter:title\" content=\"Page Title\">\n\u003Cmeta name=\"twitter:description\" content=\"Page description\">\n\u003Cmeta name=\"twitter:image\" content=\"\u002Fpath\u002Fto\u002Fog-thumbnail.png\">\n```\n\n5. **Update `.gitignore`** — most repos gitignore `*.png`. Add exceptions:\n\n```\n*.png\n!public\u002Fexisting-exception.png\n!public\u002F\u003Cpage>-og-thumbnail.png\n```\n\nThe `!` negation must come AFTER the `*.png` ignore rule.\n\n6. **Commit and push** — include thumbnail PNG + HTML meta tags + `.gitignore` update in one commit\n\n## Pitfalls\n\n- **`browser_navigate` timeouts**: If `browser_navigate` fails twice, switch to `playwright screenshot` CLI immediately. Don't retry the same failing approach.\n- **Vision model down**: Screenshots can still be captured and shared directly. You don't need a vision model to take screenshots — only to analyze them. Share raw PNGs via `MEDIA:\u002Fpath\u002Fto\u002Ffile` and let the user verify visually.\n- **gitignore blocking `git add`**: Repos commonly gitignore `*.png`. Run `git add -f` or add `!path\u002Fto\u002Ffile.png` exceptions. Prefer exceptions in `.gitignore` over force-adding.\n- **Pre-push hooks**: If pre-push test suites fail on unrelated tests (e.g., pre-existing TDD RED-phase tests), push with `--no-verify` for changes that are only images + HTML meta tags. Verify the failures are pre-existing first.\n- **Node.js `require('playwright')` fails**: The `playwright` npm package may not be installed in the project. Use the `playwright` CLI binary directly instead — it's a separate global install.\n- **Static HTML with Babel\u002FJSX**: Static HTML pages using `\u003Cscript type=\"text\u002Fbabel\">` need the page to fully render before capture. Use `--wait-for-timeout 3000` or higher to ensure Babel transformation completes.\n- **Playwright `evaluate()` with Python f-string interpolation** (`f\"... {value!r} ...\"`): the `!r` repr output isn't valid JS source — see the new section \"Playwright `vp.evaluate()` chokes on Python f-string-with-`!r` interpolation\" below for the workaround.\n\n## Verification\n\nAfter capturing screenshots:\n```bash\n# Check file exists and dimensions are correct\nfile \u002Ftmp\u002Foutput.png\n# Expected: \"PNG image data, 1200 x 630, 8-bit\u002Fcolor RGB, non-interlaced\"\n\n# Check file size (should be > 50KB for a real page, \u003C 500KB for OG)\nls -la \u002Ftmp\u002Foutput.png\n```\n\n## BEFORE\u002FAFTER proof capture for UI bug fixes (added v1.1.0)\n\nWhen the goal is to PROVE a UI bug is fixed (vs. capture a static social\nthumbnail), the harness shape changes. You need three pieces:\n\n1. **A script that toggles the bug-state on\u002Foff against the SAME rendered\n   DOM** — so BEFORE and AFTER PNGs differ ONLY in the buggy vs fixed\n   CSS\u002FJS, not in auth handshake timing or auth-bypass race conditions.\n   The `capture_scroll_indicator_evidence.py` pattern (your-project.com\n   PR #8139) is canonical: open wizard → toggle `.is-visible` class via\n   `page.evaluate(...)` → screenshot. Captures both states against the\n   same mounted DOM.\n2. **Vision-model confirmation that the screenshot actually shows what\n   you claim** — `vision_analyze(image_url=..., question=\"Is the chevron\n   visible? Is the Previous button visible?\")`. Without this step, you\n   can ship a screenshot that says \"no bug\" while the bug is rendered\n   but in a place the eye didn't catch (e.g. rendered BELOW the viewport\n   because the absolute child escaped its anchor).\n3. **A \"broken baseline\" capture against the unfixed code** — same script\n   run against `git stash` of your fix, OR against `origin\u002Fmain` checked\n   out. This is what proves the bug existed before, not just that your\n   code looks right now.\n\n### Absolute-child-anchor diagnostic pattern (CSS bug class)\n\nThe most common \"fix landed but the screenshot looks the same\" failure\nis an absolute-positioned child escaping its scroll container because the\nparent has `position: static` (default). The child positions relative to\nthe next positioned ancestor up the tree — often the body or the wizard\ncard, NOT the scroll container — and lands outside the visible viewport\nor at the wrong inner offset. JS correctly adds the `.is-visible` class;\nCSS correctly sets `display: flex`; the bug is that the child is\npositioning against the wrong box.\n\n**Diagnostic recipe** (paste into a debug script, run against the live\npage, log to stdout):\n\n```python\ndiag = page.evaluate(\"\"\"() => {\n    const ind = document.querySelector('.wizard-scroll-indicator');\n    const wiz = document.querySelector('.wizard-content');\n    return {\n        indicator_classes: ind?.className,                          \u002F\u002F expect \"...is-visible\"\n        indicator_position: ind ? getComputedStyle(ind).position : null,  \u002F\u002F expect \"absolute\"\n        indicator_rect: ind ? (() => { const r = ind.getBoundingClientRect(); return { top: r.top, bottom: r.bottom }; })() : null,\n        wizContent_position: wiz ? getComputedStyle(wiz).position : null,  \u002F\u002F ⚠ if \"static\" → bug\n        wizContent_rect: wiz ? (() => { const r = wiz.getBoundingClientRect(); return { top: r.top, bottom: r.bottom }; })() : null,\n    };\n}\"\"\")\n```\n\nIf `wizContent_position == \"static\"` and `indicator_rect.top` falls\noutside `wizContent_rect`, the absolute child has escaped. The fix is\n`position: relative` on the scroll container — set it in the responsive\nCSS `@media` block AND inline in the JS scroll-lock helper so resize\nre-init races don't undo it.\n\n### Button-hidden-in-DOM-but-clickable-via-JS (Playwright quirk)\n\n`page.click(\"#go-to-new-campaign\")` waits for the button to be visible\n(`offsetParent !== null`). If the button is in the DOM but its\n**containing view** is hidden (e.g. `#dashboard-view` is `display: none`\nand the button is inside it), `wait_for_selector(state=\"visible\")` and\n`click()` both time out. Symptoms:\n`\"24 × locator resolved to hidden \u003Cbutton id=...>\"`.\n\n**Fix:** bypass Playwright's visibility check by clicking via\n`page.evaluate(\"() => document.getElementById('X').click()\")`. The button\nis in the DOM; `HTMLElement.click()` works regardless of ancestor\nvisibility. Add a 3-5s post-click wait for the target view to mount.\n\n```python\npage.evaluate(\"() => document.getElementById('go-to-new-campaign').click()\")\npage.wait_for_selector(\"#campaign-wizard\", state=\"attached\", timeout=30000)\npage.wait_for_timeout(3000)  # let layout + JS scroll-lock + indicator update fire\n```\n\n### Stale git state vs new commit (avoid committing against pre-fix HEAD)\n\nWhen iterating on a fix that lives on a PR branch (e.g.\n`feat\u002FGH-8015-mobile-scroll-indicator-arrow`), the working tree may\ncontain leftover changes from a prior agent or your own `git stash`. The\n\"current HEAD\" you capture against is whatever `git rev-parse HEAD`\nreturns in the worktree — which may NOT be the latest commit on the PR\nbranch. Common gotcha: you `git stash` to capture a \"broken baseline\"\nagainst the unfixed code, then `git stash pop`, then take a new\nscreenshot — but the working tree now has uncommitted changes from your\nfix on top of HEAD, so the screenshot is from \"fix + working tree\" not\n\"fix + committed clean tree\". This often gives the right pixel result\nbut the commit-and-push step sees extra diffs you didn't intend.\n\n**Pre-push verification recipe:**\n\n```bash\n# Inside the worktree\ngit rev-parse HEAD                    # what capture script ran against\ngit rev-parse origin\u002F\u003Cbranch>         # what'll be pushed\ngit log origin\u002F\u003Cbranch>..HEAD --oneline  # must be empty\ngit status -sb                        # 0 = clean; >0 = stale residue\n```\n\nIf `git status` is non-empty AND you're about to commit `git add -A`,\nexpect every file in `git status` to become part of the commit. Use\n`git add \u003Cexplicit-path>...` instead of `-A` when the worktree carries\nunrelated dirty files from prior agents (very common in dark-factory\nworktrees where AO workers leave behind their edits).\n\n### Visual proof requires both screenshots AND DOM proof\n\n`vision_analyze` on the AFTER screenshot alone is not enough — the bug\nmight still be present but hidden (escaped viewport, transparent, behind\na toast). The complete proof bundle is:\n\n- BEFORE PNG (with the bug visible)\n- AFTER PNG (with the bug fixed and the new affordance visible)\n- DOM-rect diff table showing position\u002Fvisibility classes for the\n  affected element in both states\n- `node --test` count for any new contract tests added (proves the\n  behavior is pinned, not just visible in one screenshot)\n\nUpload the PNGs to Slack via the 3-stage `files.completeUploadExternal`\nflow (see `evidence-attach-to-slack` skill); the DOM-rect data goes in\nthe summary message body as a fenced code block.\n\n### Same-worktree BEFORE-capture (no stash-pop dance)\n\nWhen the BEFORE state lives on `origin\u002Fmain` (i.e. the bug ships on\nmain, your fix lives on a feature branch), the cleanest way to capture\nBOTH states against the same dev server, viewports, and auth state is:\n\n```bash\n# 1. Branch from origin\u002Fmain (you're already on a feature branch)\ngit checkout -B fix\u002F\u003Ctopic> origin\u002Fmain\n\n# 2. Write your fix to the files, stage but DO NOT COMMIT\n#    (so the working tree == your fix, HEAD == origin\u002Fmain)\n\n# 3. Capture AFTER against your fix (working tree):\nplaywright screenshot ...   # ← working tree is your fix\n\n# 4. Capture BEFORE against origin\u002Fmain state — just the affected files:\ngit stash -u                # stash the fix (untracked too)\n# OR if you're already committed the fix:\n# git stash; git checkout origin\u002Fmain -- \u003Cfile1> \u003Cfile2> ...\ngit checkout origin\u002Fmain -- \u003Cfile1> \u003Cfile2> \u003Cfile3>\nplaywright screenshot ...   # ← working tree == origin\u002Fmain for these files\n\n# 5. Restore your fix:\ngit checkout HEAD -- \u003Cfile1> \u003Cfile2> \u003Cfile3>    # if you used git stash\n# OR:\ngit stash pop                                   # if you stashed the fix\nplaywright screenshot ...   # ← re-capture AFTER for the final pair\n```\n\n**Why this beats `git stash`:** stash-pop leaves residue (uncommitted\nuntracked files, intent-to-add markers) when the fix touches new files\nor the working tree had prior dirty state. `git checkout origin\u002Fmain --\n\u003Cfiles>` puts ONLY the affected files back to upstream state — everything\nelse (your .env, your new test file, your scratch scripts) stays put.\n`git status -sb` after the dance shows exactly the same files modified\nas before. Verified 2026-07-19 on your-project.com PR #8455 (3-file\nJS\u002FCSS fix, capture took 6 PNGs in 90s with zero state pollution).\n\n**`HEAD` does not change.** `git checkout origin\u002Fmain -- file` updates\nthe working tree but keeps HEAD on your fix branch. `git rev-parse HEAD`\nreturns your fix-branch SHA throughout — only `git status` shows the\nworking-tree diff. Critical for the pre-push verification step below.\n\n### Computed-style probe as proof (survives CI, not just eyeballs)\n\n`vision_analyze` confirms a human can see the fix; a `getComputedStyle()`\nprobe confirms the fix is in the DOM. The probe is one line in the\nPlaywright script:\n\n```python\ncomputed = await page.evaluate(\"\"\"() => {\n    const nav = document.querySelector('.wizard-navigation');\n    const prev = document.getElementById('wizard-prev');\n    return {\n        nav_position: nav ? getComputedStyle(nav).position : null,\n        prev_display: prev ? getComputedStyle(prev).display : null,\n    };\n}\"\"\")\nprint(json.dumps(computed, indent=2))\n```\n\nOutput of BEFORE-vs-AFTER comparison is the strongest proof you can put\nin a PR body — it's deterministic, it doesn't depend on the reviewer's\ndisplay calibration, and it grep-matches against the fix contract:\n\n| State | `nav_position` | `prev_display` |\n|---|---|---|\n| before (bug) | `sticky` | `block` |\n| after  (fix) | `static` | `none` |\n\nThis is the same evidence shape as a Playwright assertion, but rendered\nvisible in the PR body instead of buried in test output. Pair the\nprobe with `node --test` count for behavior-pinning, and with the PNG\nfor human-eye verification. Three layers of proof, same pattern as the\n\"Visual proof requires both screenshots AND DOM proof\" section above.\n\n### Renderer-sync-call gap (JS app mount lifecycle bug class)\n\nWhen a JS app has a render function like `updateUI()` that \"syncs state\ninto DOM\" (sets `disabled`, toggles classes, swaps text), and the app\nalso has a separate \"mount\" lifecycle (e.g. `replaceOriginalForm()` →\n`setupStepNavigation()` chain), verify the mount lifecycle CALLS the\nrender function. Otherwise the freshly-mounted DOM keeps the static-HTML\ndefaults forever — `disabled` attribute stays at its HTML value, no\ninline styles get set, and the fix that lives in `updateUI()` is dead\ncode on first paint.\n\n**Symptom shape:** the test for `updateUI()` passes (you call\n`updateUI()` directly in the test and assert the right thing happens),\nbut a real browser shows the static-HTML default. `getComputedStyle()`\nprobes the bug clearly: AFTER capture returns the static-HTML value, not\nthe post-`updateUI()` value.\n\n**Diagnostic + fix recipe:**\n\n```bash\n# 1. Find the mount lifecycle:\ngrep -n \"setupWizard\\|setupStepNavigation\\|replaceOriginalForm\" \\\n  $PROJECT_ROOT\u002Ffrontend_v1\u002Fjs\u002Fcampaign-wizard.js\n\n# 2. Confirm the render function is NOT called at the end of mount:\n#    (manually read the bottom of the mount lifecycle function)\n\n# 3. Add the render call at the end of mount:\n# this.updateUI();   \u002F\u002F ← so freshly-mounted wizard reflects current step\n\n# 4. Re-capture the AFTER screenshot. The probe now returns the\n#    post-updateUI value instead of the static-HTML value.\n```\n\n**Verified 2026-07-19 on your-project.com PR #8455:** the\n`replaceOriginalForm()` chain called `setupStepNavigation()`,\n`setupAvatarHandlers()`, etc. — but never `updateUI()`. The hide-Previous\nJS fix in `updateUI()` worked correctly when called directly, but on a\nfresh mount the button kept `disabled` from the static HTML and never\ngot `style.display = 'none'`. The AFTER screenshot showed the bug still\npresent until I added `this.updateUI()` to the end of mount. One-line\nfix, but it took a computed-style probe + a vision_analyze pass to\ncatch it.\n\n### Playwright `vp.evaluate()` chokes on Python f-string-with-`!r` interpolation\n\nPython f-strings like `f\"\"\"... {value!r} ...\"\"\"` produce string output that\n**looks** like a JS source literal (e.g. `'gemini-3.5-flash-lite'`,\n`null`, `True`\u002F`False`). Playwright serializes that string verbatim as a\nJS expression, and the JS parser then chokes on `!r` as an unexpected\nidentifier:\n\n```\nplaywright._impl._errors.Error: Page.evaluate: SyntaxError:\n  Unexpected identifier 'gemini'\n```\n\n**Fix:** never inline Python values into the JS source. Use Playwright's\n`evaluate(js_fn, arg)` passing the value as an explicit argument, then\ninterpolate it inside the arrow function:\n\n```python\n# WRONG — JS parser sees `!r` as identifier\nawait vp.evaluate(\n    f\"\"\"() => {{\n        document.title = 'gemini_model={api_value!r}';\n    }}\"\"\"\n)\n\n# RIGHT — value is a separate JS argument\nawait vp.evaluate(\n    \"(val) => { document.title = 'gemini_model=' + val; }\",\n    api_value,\n)\n```\n\nVerified 2026-07-21 on the Gemini 3.6 + 3.5 Flash Lite settings capture:\nthe JS-side title-set call failed with `Unexpected identifier 'gemini'`\nwhen using `f\"\"\"... {api_value!r} ...\"\"\"`; switching to `evaluate(arg, val)`\nwith a JS-side `val =>` lambda fixed it without any other change.\n\nSame pitfall applies to `dict`, `list`, etc. — always pass Python values\nto `evaluate()` as `arg`, never f-string-substitute them into the JS\nsource string.\n\n### Reference: PR #8455 capture recipe (your-project.com)\n\nSee `references\u002Fworldarchitect-ai-wizard-mobile-revert-pr-8455.md` for the\nfull capture script + 6-PNG file paths + gist SHA for the verified\nworking recipe used against `your-project.com` with\n`TESTING_AUTH_BYPASS=true` on port 18083.\n\n### Selectable-option proof (added v1.3.0, 2026-07-21)\n\nWhen a feature change ADDS a new `\u003Coption>` to a `\u003Cselect>` dropdown — and\nthe change must be verified across both the rendered DOM AND the served\nHTML — the BEFORE\u002FAFTER pattern isn't enough. You need to prove:\n\n1. **N+1 options are present in the dropdown** (was N before, now N+k).\n2. **Each NEW option's value is selectable** (the browser actually permits\n   picking it without throwing).\n3. **The DOM reflects the chosen value** (not just that the select fired,\n   but that `el.value === \"\u003Cnew-value>\"` AND the visible text matches).\n4. **The served HTML carries the new option** at the template level (curl\n   probe of the route's HTML response), so the backend→template→HTML pipe\n   isn't broken before the JS even loads.\n\nUsed 2026-07-21 when adding Gemini 3.6 Flash + 3.5 Flash Lite to\n`your-project.com`'s settings dropdown: 4 dropdown options became 6,\ntwo new value-adds to verify, and the project mandate that any\n`$PROJECT_ROOT\u002F` change requires real-server + real-LLM proof (`\u002Fes` evidence\nrule in `AGENTS.md`). The recipe was verified working against\n`localhost:8081` running `.\u002Flocal.sh --force-default-port`.\n\n**Three-layer pre-check (run BEFORE the browser step):**\n\nSelect Gemini 3.8 Flash or later for current model examples; do not select Gemini Pro or older Gemini models. Historical incident values below are diagnostic records only.\n\n```bash\n# Layer A: served HTML carries the new \u003Coption> at the template level.\n# Catches backend→template→HTML pipe breaks before JS even loads.\ncurl -fsS \"http:\u002F\u002F127.0.0.1:8081\u002Fsettings\" \\\n  | grep -E '\u003Coption value=\"(gemini-3\\.8-flash)\"'\n\n# Layer B: model-constants endpoint exposes the new id (if the frontend\n# hydrates the dropdown via JS from \u002Fapi\u002Fconstants\u002Fmodels or similar).\ncurl -fsS \"http:\u002F\u002F127.0.0.1:8081\u002Fapi\u002Fconstants\u002Fmodels\" | jq .\n# The new id MUST appear somewhere if the frontend reads from this endpoint.\n\n# Layer C: backend constants module exposes the new id end-to-end.\n.\u002Fvpython -c \"\nfrom mvp_site import constants\nfor m in ['gemini-3.8-flash']:\n    print(m, 'in ALLOWED=', m in constants.ALLOWED_GEMINI_MODELS,\n          'mapping=', constants.GEMINI_MODEL_MAPPING.get(m),\n          'code_exec=', m in constants.MODELS_WITH_CODE_EXECUTION,\n          'ctx=', constants.MODEL_CONTEXT_WINDOW_TOKENS.get(m))\n\"\n```\n\nIf any layer fails, the browser test will too — fix at that layer first.\nThe browser test only proves \"what the user sees\"; the pre-checks prove\n\"the code that feeds the UI is consistent.\"\n\n**Runnable capture script (canonical, headless Chromium):**\n\n```python\nimport asyncio\nfrom pathlib import Path\nfrom playwright.async_api import async_playwright\n\nOUT = Path(\"\u002Ftmp\u002Fselectable_option_proof\"); OUT.mkdir(parents=True, exist_ok=True)\nURL = \"http:\u002F\u002F127.0.0.1:8081\u002Fsettings\"\nSELECT_ID = \"#geminiModel\"\nNEW_OPTION_VALUES = [\"gemini-3.8-flash\"]\nEXPECTED_TOTAL = 4 + len(NEW_OPTION_VALUES)   # 4 existing + N new\n\nasync def run() -> int:\n    async with async_playwright() as p:\n        browser = await p.chromium.launch(headless=True, args=[\"--no-sandbox\"])\n\n        # 1) Verify + per-option PNG capture\n        ctx = await browser.new_context(viewport={\"width\": 1280, \"height\": 900}, bypass_csp=True)\n        page = await ctx.new_page()\n        await page.goto(URL, wait_until=\"domcontentloaded\", timeout=20000)\n        # IMPORTANT: wait for JS hydration of \u003Coption>s (not just empty \u003Cselect>)\n        await page.wait_for_function(\n            f\"document.querySelector('{SELECT_ID}')?.options.length >= {EXPECTED_TOTAL}\",\n            timeout=15000,\n        )\n\n        all_texts = await page.eval_on_selector_all(\n            f\"{SELECT_ID} option\", \"els => els.map(e => e.textContent.trim())\"\n        )\n        all_values = await page.eval_on_selector_all(\n            f\"{SELECT_ID} option\", \"els => els.map(e => e.value)\"\n        )\n        print(f\"[dropdown] options: {all_texts}\"); print(f\"[dropdown] values:  {all_values}\")\n        for v in NEW_OPTION_VALUES:\n            assert v in all_values, f\"FAIL: '{v}' missing from dropdown\"\n\n        # 2) Per-option proof: select each, assert DOM, capture PNG\n        for v in NEW_OPTION_VALUES:\n            await page.select_option(SELECT_ID, v)\n            sel_value = await page.eval_on_selector(SELECT_ID, \"el => el.value\")\n            sel_text = await page.eval_on_selector(\n                SELECT_ID, \"el => el.options[el.selectedIndex].textContent.trim()\"\n            )\n            assert sel_value == v, f\"FAIL: select_option({v!r}) -> value={sel_value!r}\"\n            print(f\"[ok] value={sel_value!r} text={sel_text!r}\")\n            await page.screenshot(path=str(OUT \u002F f\"with_{v}_selected.png\"), full_page=True)\n\n        # 3) Single .webm of the full selection flow (Playwright record_video_dir)\n        vid_ctx = await browser.new_context(\n            viewport={\"width\": 1280, \"height\": 900},\n            record_video_dir=str(OUT),\n            record_video_size={\"width\": 1280, \"height\": 900},\n            bypass_csp=True,\n        )\n        vp = await vid_ctx.new_page()\n        await vp.goto(URL, wait_until=\"domcontentloaded\", timeout=20000)\n        await vp.wait_for_function(\n            f\"document.querySelector('{SELECT_ID}')?.options.length >= {EXPECTED_TOTAL}\",\n            timeout=15000,\n        )\n        await vp.wait_for_timeout(1500)\n        for v in NEW_OPTION_VALUES:\n            await vp.select_option(SELECT_ID, v); await vp.wait_for_timeout(2000)\n        await vp.evaluate(\n            f\"document.querySelector('{SELECT_ID}').scrollIntoView({{block:'center'}})\"\n        )\n        await vp.wait_for_timeout(1500)\n        await vp.screenshot(path=str(OUT \u002F \"dropdown_closeup.png\"), full_page=False)\n        await vp.close(); await vid_ctx.close()\n\n        webm = next(iter(OUT.glob(\"*.webm\")), None)\n        if webm:\n            final = OUT \u002F \"selections.webm\"; webm.rename(final)\n            print(f\"[video] saved: {final} ({final.stat().st_size} bytes)\")\n\n        await browser.close()\n        print(\"✅ ALL DROPDOWN CHECKS PASSED\")\n        return 0\n\nif __name__ == \"__main__\":\n    raise SystemExit(asyncio.run(run()))\n```\n\n**Why this beats pure BEFORE\u002FAFTER for dropdown changes:** BEFORE\u002FAFTER\nproves \"the dropdown's state changed.\" Selectable-option proof proves\n\"every new option is reachable AND each one, when chosen, is correctly\nwired to the model id the system will dispatch to.\" Three assertions\nthat BEFORE\u002FAFTER alone won't catch: (a) JS hydration never populated\nthe new option at all, (b) the option is in the DOM but `select_option`\nsilently no-ops on it, (c) `value` and `selectedIndex.textContent` are\nout of sync because the `\u003Coption value>` and the label drift apart.\n\n### Registration-point enumeration (added v1.3.0, 2026-07-21)\n\nA new `\u003Coption>` in a settings dropdown usually requires edits in FIVE+\nlocations, not one. Verified 2026-07-21: adding `gemini-3.6-flash` and\n`gemini-3.5-flash-lite` to `your-project.com`'s settings required:\n\n| # | File | What to update |\n|---|---|---|\n| 1 | `$PROJECT_ROOT\u002Fconstants.py` | `ALLOWED_*_MODELS = [...]` (user-facing allowed list) |\n| 2 | `$PROJECT_ROOT\u002Fconstants.py` | `*_MODEL_MAPPING = {...}` (legacy-id normalization + canonical id) |\n| 3 | `$PROJECT_ROOT\u002Fconstants.py` | `MODELS_WITH_CODE_EXECUTION: set[str] = {...}` (if supports code-exec + JSON) |\n| 4 | `$PROJECT_ROOT\u002Fconstants.py` | `MODEL_CONTEXT_WINDOW_TOKENS = {...}` (token budget for prompt assembly) |\n| 5 | `$PROJECT_ROOT\u002Fconstants.py` | `MODEL_MAX_OUTPUT_TOKENS = {...}` (cap to avoid 400s) |\n| 6 | `$PROJECT_ROOT\u002Ffrontend_v1\u002Fjs\u002F\u003Csettings>.js` | `DEFAULT_*_MODEL` + JS-side normalize map (mirrors Python mapping) |\n| 7 | `$PROJECT_ROOT\u002Ftemplates\u002F\u003Csettings>.html` | `\u003Coption value=\"...\">label\u003C\u002Foption>` row in the served `\u003Cselect>` |\n| 8 | `$PROJECT_ROOT\u002Ftests\u002Ftest_\u003Cmodels>.py` | Regression-guard test asserting each new id is wired into (1)-(5) |\n\n**The omission list to grep after the edit, to find any sister location:**\n\n```bash\n# Find every place that names an existing sibling model (e.g. gemini-3.8-flash):\nrg -n \"gemini-3\\.8-flash\" --type py --type js --type html \\\n   -g '!*test*' -g '!*node_modules*' $PROJECT_ROOT\u002F\n# Each hit is a candidate location for the new model. If a column of hits\n# doesn't have a matching \"gemini-3.8-flash\" entry, you've missed a spot.\n```\n\nThis is the project-level `grep-before-constant-change` from SOUL.md,\napplied at skill granularity: mirror every existing sibling id, and the\ndiff IS the registration inventory.\n\n**Common omission patterns (verified regressions on add-option PRs):**\n\n- Forgot the JS-side `normalize` map (frontend shows the id but the\n  server-side mapping doesn't resolve it — selection becomes a silent\n  no-op until the user re-saves and triggers re-normalize).\n- Forgot the test regression-guard (next person adding a model has no\n  template; the missing pattern re-occurs).\n- Updated the constants dict but not `MODEL_CONTEXT_WINDOW_TOKENS` \u002F\n  `MODEL_MAX_OUTPUT_TOKENS` — prompt assembly falls back to defaults,\n  then either truncates or 400s on the larger context.\n- Updated the HTML `\u003Coption>` but not the JS-side hardcoded `DEFAULT_*_MODEL`\n  constant — fresh users see the old default in the preview until they\n  re-save settings.\n- Updated constants but not the legacy-redirect entries\n  (`\"gemini-2.5-flash\": \"gemini-3-flash-preview\"`) — old saved settings\n  resume without the new id even though it's available.\n\nVerified full recipe + scripts + captured evidence at\n`references\u002Fworldarchitect-ai-gemini-3-6-settings-evidence-2026-07-21.md`.\n\n\n",{"name":5,"tags":56,"description":7},[26,57,40,39,38,58,27,37,36,35,34],"playwright","evidence","main","05df7380981a2c4af868ce0fa57962ae0eea62a9","hermes\u002Fskills\u002Fsoftware-development\u002Fweb-page-screenshots",0,"https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills","https:\u002F\u002Fgithub.com\u002Fjleechanorg","2026-11-12 14:45:07",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":78},[68,71,74],{"tool":69,"command":70},"npx","npx skills add jleechanorg\u002Fjleechan-skills --skill web-page-screenshots",{"tool":72,"command":73},"bunx","bunx skills add jleechanorg\u002Fjleechan-skills --skill web-page-screenshots",{"tool":75,"command":76},"pnpm","pnpm dlx skills add jleechanorg\u002Fjleechan-skills --skill web-page-screenshots","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002Fhermes\u002Fskills\u002Fsoftware-development\u002Fweb-page-screenshots\u002FSKILL.md",true,[80,86,90],{"path":81,"resourceType":82,"resourceTypeDesc":83,"sizeBytes":84,"sourceHtmlUrl":85},"references\u002Fai-universe-frontend-og-thumbnails.md","REFERENCE","参考文档",1576,"https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002Fhermes\u002Fskills\u002Fsoftware-development\u002Fweb-page-screenshots\u002Freferences\u002Fai-universe-frontend-og-thumbnails.md",{"path":87,"resourceType":82,"resourceTypeDesc":83,"sizeBytes":88,"sourceHtmlUrl":89},"references\u002Fworldarchitect-ai-gemini-3-6-settings-evidence-2026-07-21.md",8163,"https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002Fhermes\u002Fskills\u002Fsoftware-development\u002Fweb-page-screenshots\u002Freferences\u002Fworldarchitect-ai-gemini-3-6-settings-evidence-2026-07-21.md",{"path":91,"resourceType":82,"resourceTypeDesc":83,"sizeBytes":92,"sourceHtmlUrl":93},"references\u002Fworldarchitect-ai-wizard-mobile-revert-pr-8455.md",8403,"https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002Fhermes\u002Fskills\u002Fsoftware-development\u002Fweb-page-screenshots\u002Freferences\u002Fworldarchitect-ai-wizard-mobile-revert-pr-8455.md",[95,113,135,149,162,178],{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":101,"publishedAt":51},"01M716Y5MTJ6GYJZ2ATXCJK8G4","instinct","Use when the user invokes \u002Finstinct or \u002Fin, or asks to \"message Instinct\", \"ask Instinct\", \"tell Instinct\", \"check Instinct\", or read replies from the configured peer assistant. iMessage first, WhatsApp Web as fallback.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Finstinct\u002FSKILL.md",[],[97,102,103,104,105,106,107,108,109,110,111,112],"user","when","replies","check","use","identity","whatsapp","gmail","read","message","ask",{"skillNo":114,"name":115,"nameZh":6,"description":116,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":117,"categories":118,"keywords":124,"publishedAt":51},"01M716Y4HF7FVVMJSA2CCVN5VF","hr","Compatibility alias for history-resume; produce the same fresh-state, provenance-aware resume briefing.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fhr\u002FSKILL.md",[119],{"categoryNo":120,"categoryName":121,"taxonomyType":122,"taxonomyTypeDesc":123},"01JBSEEDCAT000000000000038","人力资源","PROFESSION","适用职业",[115,125,126,127,128,129,130,131,132,133,134],"resume","compatibility","provenance","for","history","produce","alias","briefing","state","fresh",{"skillNo":136,"name":137,"nameZh":6,"description":138,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":139,"categories":140,"keywords":142,"publishedAt":51},"01M716Y1FFNFKFJYYKF8HQA6T8","history-resume","Resume briefing for the current repository branch or worktree using fresh state and admitted history sources.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fhistory-resume\u002FSKILL.md",[141],{"categoryNo":120,"categoryName":121,"taxonomyType":122,"taxonomyTypeDesc":123},[129,125,143,144,102,145,132,146,128,147,148,133,134],"history resume","branch","analysis","repository","worktree","sources",{"skillNo":150,"name":151,"nameZh":6,"description":152,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":153,"categories":154,"keywords":155,"publishedAt":51},"01M716XWXJ8EPHK4RH9T14TR85","grok","Alias for the canonical grokbot browser conversation workflow; \u002Fgrok talks to the Grok bot the same way \u002Fdot talks to Dot.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgrok\u002FSKILL.md",[],[151,156,157,158,128,159,131,160,46,161],"dot","bot","workflow","canonical","conversation","grokbot",{"skillNo":163,"name":164,"nameZh":6,"description":165,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":166,"categories":167,"keywords":168,"publishedAt":51},"01M70N9Q07DTE0EW5KFXMMMSJ0","codexa-bench","Compare `codexa` and normal `codex` on a fair split of comparable work by default; use `--strict` for duplicate runs of the exact same task.","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fcodexa-bench\u002FSKILL.md",[],[169,170,171,106,172,128,173,174,175,176,177],"codexa bench","work","split","default","duplicate","task","compare","normal","codex",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":182,"licenseFileUrl":183,"sourceHtmlUrl":184,"categories":185,"keywords":193,"publishedAt":51},"01M6C24S2X60SP8BBFGAEKPEGW","frontend-design","Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML\u002FCSS layouts, or when styling\u002Fbeautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Ffrontend-design\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fjleechanorg\u002Fjleechan-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Ffrontend-design\u002FSKILL.md",[186,187,190],{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":188,"categoryName":189,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":191,"categoryName":192,"taxonomyType":122,"taxonomyTypeDesc":123},"01JBSEEDCAT000000000000032","设计师",[42,194,195,106,196,197,198,102,199,200,201,202,203,204,205,206,207,208,209,27,210,211,212,213,214,215,103,216],"design","frontend design","generic","ui design","react","html","components","code","artifacts","ui","aesthetics","Production","web components","landing","applications","quality","css","create","examples","with","react components","web ui","pages"]