[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6J43VRP37V9PTSTZ1N1CRJ2":3,"skill-same:01M6J43VRP37V9PTSTZ1N1CRJ2":148},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":48,"linkOnly":49,"markdownContent":50,"markdownBody":51,"frontMatter":52,"refName":53,"commitSha":54,"skillDirectory":55,"forksCount":14,"repositoryHtmlUrl":56,"repositoryHomepageUrl":6,"ownerHtmlUrl":57,"ownerBlogUrl":6,"ownerDescription":58,"updatedAt":59,"install":60,"resources":73},"01M6J43VRP37V9PTSTZ1N1CRJ2","persona-test",null,"Persona-driven exploratory browser testing against a live URL.\nDrives a browser as a specific user persona using a Plan → Act → Reflect loop,\ntakes screenshots at each step, analyses UX and functional issues with\nconfidence scoring, and returns a structured P0–P3 severity report plus a\nqualitative persona debrief. Personas are tracked per app URL; pair mode\nruns two opposed personas back-to-back; consistency mode runs a\ndeterministic code-driven canary. Use for exploratory QA against deployed\napps — not scripted regression tests. Invoke on the request's shape; don't\ncheck for browser tools first and decline — the skill resolves its own\ndriver (Playwright MCP, host tools, BrightData, or read-only) once invoked.\nTriggers on: \"persona test\", \"test as\", \"explore the app as\", \"run persona\ntest\", \"test the site as\", \"browse the app as\", \"QA as\", \"list personas\",\n\"add persona\", \"who are my personas\", \"which persona should test\".\nFull command syntax: see the Usage section in this skill.","Lbstrydom\u002Fclaude-engineering-skills","Lbstrydom","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F77991486?v=4","01M6J3X4N2T0FTZT1RKFXDVS0K","Louis Strydom","01M6J3X4SXV0BXWDMXPW90D8BK",0,"MIT","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"test","persona test","persona","command","invoke","mode","which","URL","functional","apps","host","app","scoring","testing","plan","loop","explore","browser testing","run","reflect","MCP","2026-11-10 10:12:30",false,"---\nname: persona-test\ndescription: |\n  Persona-driven exploratory browser testing against a live URL.\n  Drives a browser as a specific user persona using a Plan → Act → Reflect loop,\n  takes screenshots at each step, analyses UX and functional issues with\n  confidence scoring, and returns a structured P0–P3 severity report plus a\n  qualitative persona debrief. Personas are tracked per app URL; pair mode\n  runs two opposed personas back-to-back; consistency mode runs a\n  deterministic code-driven canary. Use for exploratory QA against deployed\n  apps — not scripted regression tests. Invoke on the request's shape; don't\n  check for browser tools first and decline — the skill resolves its own\n  driver (Playwright MCP, host tools, BrightData, or read-only) once invoked.\n  Triggers on: \"persona test\", \"test as\", \"explore the app as\", \"run persona\n  test\", \"test the site as\", \"browse the app as\", \"QA as\", \"list personas\",\n  \"add persona\", \"who are my personas\", \"which persona should test\".\n  Full command syntax: see the Usage section in this skill.\n---\n\n> **Worktree preflight** — in a linked git worktree the synced tooling tree\n> `scripts\u002F.claude-skills\u002F` is absent — it is gitignored, so `git worktree add`\n> does not populate it, and every command below that uses it dies on a bare\n> `MODULE_NOT_FOUND`. Run `npm run skills:hydrate` first. If this repo defines\n> no such script, `references\u002Fworktree-preflight.md` holds the entry to add — ask\n> the user first, since it edits their `package.json`.\n\n## Usage\n\n```\nUsage:\n  \u002Fpersona-test list [url]                                                       — show all personas for an app\n  \u002Fpersona-test add \"\u003Cname>\" \"\u003Cdescription>\" \u003Curl> [app name]                    — register a new persona\n  \u002Fpersona-test \"\u003Cpersona or name>\" \u003Curl> [focus] [--device \u003Cpreset>]            — run an exploratory test (device auto-resolved from persona description)\n  \u002Fpersona-test --pair \"\u003Cp1>\" \"\u003Cp2>\" \u003Curl> [focus] [--device \u003Cpreset>]           — pair mode (--device overrides both personas)\n  \u002Fpersona-test --mode consistency --canary \u003Cname> \u003Curl>                         — deterministic consistency canary (code-driven Playwright)\nDevice presets: desktop (default fallback) | desktop-large | tablet | mobile | mobile-small\nExamples:\n  \u002Fpersona-test list https:\u002F\u002Fmyapp.railway.app\n  \u002Fpersona-test add \"Pieter\" \"wine enthusiast, 40s, drinks daily, mobile-first\" https:\u002F\u002Fmyapp.railway.app \"Wine Cellar App\"\n  \u002Fpersona-test \"Pieter\" https:\u002F\u002Fmyapp.railway.app \"adding a bottle\"\n  \u002Fpersona-test \"first-time user on mobile\" https:\u002F\u002Fmyapp.railway.app\n  \u002Fpersona-test \"Pieter\" https:\u002F\u002Fmyapp.railway.app --device mobile-small      — override resolved device\n  \u002Fpersona-test --pair \"Elena (sommelier)\" \"Martha (newer drinker)\" https:\u002F\u002Fmyapp.railway.app \"browsing the cellar\"\n  \u002Fpersona-test --mode consistency --canary oliver-infeasible-reorg http:\u002F\u002Flocalhost:3000\n```\n\n# Persona-Driven Browser Testing\n\n> **This skill's last line is an end marker naming the skill between em-dashes, after\n> the Reference files table.** If that last line is not in your context (context\n> compaction keeps only the start of a skill), re-read\n> `.claude\u002Fskills\u002Fpersona-test\u002FSKILL.md` in full before continuing — the later steps still apply.\n\nRun an exploratory browser test with persona tracking. Check `$ARGUMENTS`\nfirst to pick the sub-command.\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n\n**Where `$ARGUMENTS` comes from** — orchestrator-supplied input first, else\nthe host's verbatim invocation suffix, else the span of the user's **current**\nmessage naming this skill or its subject. Never inferred from surrounding\nconversation. This site is `subcommand`; on empty input, print usage and stop — a persona and a URL are both required, and there is no safe default target to browse.\nFull contract: `references\u002Finput-acquisition.md`.\n\n\n---\n\n## Phase 0 — Route the Command\n\nRead the first word of `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — no first word means no sub-command; fall through to the test-argument parse, which stops with usage if a persona or URL is missing._\n\n\n- `list` → **Sub-command: LIST**\n- `add` → **Sub-command: ADD**\n- contains `--mode consistency` anywhere → **Phase 3b: Consistency Mode** (deterministic canary run; skips Phases 0b–3a entirely — it needs only `--canary \u003Cname>` + URL, no persona resolution)\n- contains `--pair` anywhere → **Sub-command: PAIR** (see Phase 7 at the end)\n- otherwise → **Phase 0b: Parse Test Arguments** (normal test run)\n\n---\n\n## Sub-command: LIST\n\n**Usage**: `list [url]`\n\nResolve the URL in order: positional argument → `PERSONA_TEST_APP_URL` env →\nask the user.\n\n**Validate the URL before it reaches a shell.** It is user- or env-derived, and\ndouble quotes do **not** neutralise `$(…)` or backticks — the same defect class\nas the ADD payload below. Parse it with `new URL(value)` and reject it outright\nif the parse fails or the string contains any of `` $ ` ; | & \u003C > ( ) `` or a\nnewline. Only a value that survived that check is substituted below:\n\n```bash\nnode scripts\u002Fcross-skill.mjs list-personas --url \"\u003Cvalidated url>\"\n```\n\nResponse `{\"ok\": true, \"cloud\": true|false, \"rows\": [\u002F* persona_dashboard rows *\u002F]}`\n(graceful no-op when cloud is off). Render a `PERSONA ROSTER — \u003Capp_name or url>`\ntable (NAME · LAST TESTED · VERDICT · SESSIONS) sorted **never-tested first,\nthen oldest last-tested**, then one `SUGGESTION:` line naming who to run next.\n\nSTOP — do not proceed to the test phases.\n\n---\n\n## Sub-command: ADD\n\n**Usage**: `add \"\u003Cname>\" \"\u003Cdescription>\" \u003Curl> [app name]`\n\nParse from `$ARGUMENTS` after `add`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — name, description and url are required; a missing one prints usage and stops rather than registering a partial persona._\n\n1. **name** — first quoted string\n2. **description** — second quoted string\n3. **url** — URL following the quoted strings\n4. **app_name** — everything after the URL (optional)\n\nIf name, description, or url is missing, output usage and STOP.\n\nUpsert (idempotent on `name + app_url`). **Write the payload to a file with the\neditor, then pass the file — never interpolate the user's strings into a\nsingle-quoted shell argument:**\n\n```bash\n# 1. Write .audit\u002Fadd-persona.json with the Write tool (NOT a shell heredoc):\n#    {\"name\":\"…\",\"description\":\"…\",\"appUrl\":\"…\",\"appName\":null}\n# 2. Pipe it on stdin — a pipe passes bytes, never shell-parsed text, and\n#    (unlike `\u003C` redirection, which PowerShell does not support) `cat | …`\n#    parses the same way in bash and PowerShell (`cat` is a built-in\n#    PowerShell alias for `Get-Content`):\ncat .audit\u002Fadd-persona.json | node scripts\u002Fcross-skill.mjs add-persona --stdin\n```\n\nDo not substitute `--name`\u002F`--description`\u002F`--url` flags — they put the free\ntext back on the command line. → `references\u002Fshell-safe-payloads.md` §ADD — read\nwhen asked why a file, or tempted to inline the strings.\n\nResponse `{\"ok\": true, \"cloud\": ..., \"personaId\": ..., \"existed\": bool}`.\nReport success with `personaId`. STOP.\n\n---\n\n## Phase 0b — Parse Test Arguments (normal test run)\n\nParse `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — persona and url are required; never substitute a previously-tested URL from earlier in the conversation._\n\n1. **persona_input** — first quoted string or first unparsed token\n2. **url** — URL in the remaining args (or `PERSONA_TEST_APP_URL` env)\n3. **focus** — any remaining text after the URL (strip out flag tokens before assigning)\n4. **device_override** — value of `--device \u003Cpreset>` if present (must be one of:\n   `desktop`, `desktop-large`, `tablet`, `mobile`, `mobile-small`). Unknown\n   preset → fail with usage. Drives Phase 1a's explicit-override branch.\n\nRequired: `persona_input` + `url`. If either is missing, output usage and STOP.\n\n### Pre-flight environment check\n\n| Env var | Purpose |\n|---|---|\n| `AUDIT_DB_URL` | Cloud store on — saves sessions, reads history, and (with a repo name) cross-references audit findings. One Postgres store backs every feature post-M4. |\n| `PERSONA_TEST_REPO_NAME` | Enables audit-loop cross-reference (`audit_link = true`) |\n\nDo not read `AUDIT_DB_URL` from the session's `process.env` — the consumer's\n`.env` is not loaded there, so it reads unset and the run silently goes\nstateless. Probe through the CLI, from the consumer-repo root:\n\n```bash\nnode scripts\u002Fcross-skill.mjs whoami\n```\n\n→ `references\u002Frun-loop-detail.md` §Pre-flight — read when the probe and the env disagree.\n\nSet `memory_enabled = .cloud` from the probe's JSON output. When `cloud`\nis false the skill runs in \"stateless\" mode — tests complete but nothing is\nsaved or cross-referenced. `audit_link` is NOT computed here — it needs\n`repo_name`, which isn't resolved until Phase 0c below; see there.\n\n---\n\n## Phase 0c — Resolve Persona\n\nIf `persona_input` matches a registered persona name (for this `url`):\n- `persona = matched.description`, `persona_id = matched.id`, `persona_name = matched.name`, `repo_name = matched.repo_name`\n\nOtherwise treat `persona_input` as an ad-hoc persona description;\n`persona_id = null`.\n\nIf `repo_name` is not on the persona, detect from `PERSONA_TEST_REPO_NAME` env,\nor `git remote get-url origin`, or leave null.\n\nNow that `repo_name` is resolved (or confirmed null), compute\n`audit_link = memory_enabled && repo_name` — cross-referencing needs both\ncloud storage AND a resolved repo identity to look findings up against.\n\n---\n\n## Phase 0d — Audit-Loop Pre-Test Enrichment\n\nSkip if `audit_link = false`. When on, fetch recent HIGH + MEDIUM audit\nfindings (with `id` + `run_id` for Phase 6b correlations) and add a\n**Known Code Fragilities** section to the persona mental model in Phase 2.\n\nFull rules + query shape: `references\u002Faudit-correlation.md`.\n\n---\n\n## Phase 1 — Detect Browser Tool\n\nResolve the driver through the **one detection oracle** —\n`references\u002Fbrowser-tool-detection.md`. Do not restate its ladder here; this\nstep only declares what persona-test needs from it.\n\n**Minimum capability sets** (§4 of the oracle):\n\n- **Full journey**: `navigate`, `click`, `type`, `evaluate`, `screenshot`,\n  `wait`, `currentUrl`.\n- **Read-only degraded**: `readText` alone (it takes a URL, so it needs no\n  `navigate`). Below that → `blocked`.\n\nSet `browser_driver` to the selected driver id (`playwright-mcp`,\n`copilot-browser`, `brightdata`, `static-fetch`) and `browser_status` to `ok`,\n`degraded` or `blocked`, then keep both for the whole session — never mix\ndriver families mid-run.\n\n**A `degraded` run is reported as degraded.** Name the missing capabilities,\nprint the `[DEGRADED MODE]` banner at the top of the report, and mark every\ninteraction, flow and state-change stage **not-run** rather than passed. A\npartial capture degrades to `unverified`; it never becomes \"verified \u002F 0\nfindings\".\n\nFull contract — capability vocabulary, driver table, selection order, status\nevidence, Windows spawn caveats: `references\u002Fbrowser-tool-detection.md`.\n\n**Login-gated target** (primary surfaces behind login): first check whether\nthe MCP session is already authenticated; if not and this is a repeat\nsession, ask whether a `--storage-state` bootstrap exists. Seed it **before\nthe MCP server connects** — `.mcp.json` is read once, so a mid-session seed is\nnever re-read. → `references\u002Frun-loop-detail.md` §Phase 1 + `references\u002Fauth-bootstrap.md`\n— read when the target is login-gated.\n\n---\n\n## Phase 1a — Device Profile Resolution (required)\n\nSkip ONLY when `browser_status = \"degraded\"` and the driver has no viewport concept.\nOtherwise, this is non-negotiable — the LLM does not pick the device.\n\n### Step 1a.1 — Get the device contract (required)\n\nRun from the consumer-repo root:\n\n```bash\nnode scripts\u002Flib\u002Fdevice-presets.mjs prep \"the persona's description, or ad-hoc persona_input text\"\n```\n\nAdd `--device PRESET` only when `$ARGUMENTS` contained an explicit override preset (Phase 0b item 4).\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=default -->\n_This site: `subcommand` — no explicit `--device` means the device is resolved from the persona description, never guessed from context._\n\nThe CLI returns a JSON contract (`device`, `expectedFirstMcpCall`,\n`personaMentalModelTags`, `logLine`). **Echo the `logLine` verbatim to stderr**\n— it is the audit trail of which preset was applied.\n\n### Step 1a.2 — Execute `expectedFirstMcpCall` verbatim\n\nCall `browser_resize` with the args from `expectedFirstMcpCall.args` —\n**before Phase 1b's first `browser_navigate`**. Do not modify the args;\ndo not pick your own dimensions. If the contract said `{width: 390,\nheight: 844}`, that's what you call.\n\n### Step 1a.3 — Apply `personaMentalModelTags` to Phase 2\n\nWhen `device.isMobile === true`, apply the tags to Phase 2's mental model\n**silently** — they shape Reflect scoring, never the Phase 5b persona voice.\n\n→ `references\u002Fdevice-profile.md` — read when you need the full contract shape,\nthe tag semantics, what viewport-only emulation does not cover (UA, touch\nevents, DPR — use `--mode consistency` for those), or the pair-mode device rule.\n\n---\n\n## Phase 1b — Service-worker cache-bust (MANDATORY for own apps)\n\nService workers silently serve stale bundles, so a deployed fix reads as \"not\ndeployed\". → `references\u002Frun-loop-detail.md` §Phase 1b.\n\nSkip when `browser_status = \"degraded\"` (no `evaluate` capability). Skip when\nthe URL is a static-hosted page with no service worker (`*.github.io`, etc.).\nOtherwise, **run this before Phase 2**:\n\n```js\n\u002F\u002F browser_evaluate\n(async () => {\n  const regs = await navigator.serviceWorker?.getRegistrations() ?? [];\n  await Promise.all(regs.map(r => r.unregister().catch(() => {})));\n  const keys = await caches?.keys() ?? [];\n  await Promise.all(keys.map(k => caches.delete(k).catch(() => {})));\n  return { unregistered: regs.length, cachesDeleted: keys.length };\n})()\n```\n\nThen `browser_navigate({url})` again to force a fresh fetch. If the evaluate\nreturns `{unregistered: 0, cachesDeleted: 0}`, the page had no SW — proceed\nwithout the reload. Log the result one-line:\n`[cache-bust] unregistered \u003Cn> SW, cleared \u003Cn> caches`.\n\n**Don't** treat a non-zero `cachesDeleted` as a finding — caches are normal.\nThe finding-worthy event is when a fix doesn't appear after cache-bust;\nthat's a real deploy failure, not a caching artefact.\n\n---\n\n## Phase 2 — Build the Persona's Mental Model\n\nBefore driving, articulate the persona's profile in 5 dimensions:\n\n| Dimension | Prompt |\n|---|---|\n| **Background** | Age range, tech comfort, relevant domain knowledge, attitudes |\n| **Intent** | What are they trying to accomplish? What success looks like to them |\n| **First actions** | The 3 things they'd naturally try within 30 seconds of landing |\n| **Patience budget** | Low (phone, distracted) \u002F Medium (desktop, curious) \u002F High (research mode) |\n| **Abandonment threshold** | What would make them close the tab \u002F uninstall |\n\nIf `audit_link = true` and Phase 0d returned candidates, append the\n**Known Code Fragilities** list (silently sharpens Reflect — do not leak\nto the persona's \"voice\").\n\n---\n\n## Phase 3 — Safety Policy + Plan→Act→Reflect Loop\n\n### Safety policy (origin boundary)\n\n- **Never navigate away** from the target hostname\n- **Never submit real payment info, real credentials, or PII**\n- **Destructive actions** (delete accounts, delete data) require a fake\n  test-only context; if unavailable, SKIP the action and log it as \"deferred\"\n- Always call `browser_close` at the end, even if the session aborts\n\n### Exploration loop (8–12 steps)\n\nEach step is **Plan → Act → Reflect → Record**:\n\n1. **Plan** — one sentence: \"This persona would next try X because Y.\"\n2. **Act** — take the action (click, type, navigate); screenshot immediately after.\n3. **Reflect** — answer:\n   - Did the observed state match the expectation? (Yes \u002F No \u002F Partial)\n   - Does anything visible suggest a P0–P3 finding? (cite the element)\n   - What does this persona try next?\n4. **Record** — append to the session work-record below. This is not\n   optional bookkeeping: Phase 5's COVERAGE block renders **only** from this\n   record, never composed fresh at report time (`references\u002Fverification-discipline.md`\n   §7's reciprocal warning).\n\nRecord a finding only when confidence ≥0.6. Below that, note it as\n\"uncertain — did not report\". Every finding needs `element`, `observed`,\n`fix`, `severity`, `confidence` — and `step`, the 1-based Act step it was\nobserved on. `step` is how the auto-correlator (Phase 6b) finds the page URL\nthe finding happened on (via `clickPath`) and compares it with the audited\nfile that serves that page; a finding without it correlates on its text alone.\n\n### Stateful mission checklist (REQUIRED when the goal changes state)\n\nA goal that **creates, submits, approves, completes, or selects an item and\nhands it to another screen** is a stateful mission: reaching the goal is not\nverifying it. → `references\u002Frun-loop-detail.md` §Phase 3 — the incident behind this.\n\nFor a stateful mission, verify each of these six steps, by screenshot, and\nrecord which you could not:\n\n| Step id | Verify |\n|---|---|\n| `initial-state` | The state before acting — the queue\u002Flist\u002Fcount as it stands |\n| `selected-identity` | The immutable identity of what was selected (id, name, row) — note it before acting |\n| `post-boundary-state` | After a rerender or a navigation boundary, the SAME identity is still the one in play |\n| `command-outcome` | The command's own outcome — the success state, not just the absence of an error |\n| `return-to-origin` | Back at the origin screen, it reflects the change |\n| `retained-history` | History\u002Fprogress survives: completed items stay visible as completed, counts agree |\n\nRecord the outcome as the work-record's `lifecycle`:\n`{\"status\": \"verified\" | \"partial\" | \"not-applicable\", \"unchecked\": [\u003Cstep ids>]}` —\n`verified` (all six checked, `unchecked: []`), `partial` (at least one not\nchecked — list exactly which), or `not-applicable` (the goal changes no state:\nreading, browsing, searching). Spend step budget on the checklist before\nexploring sideways — an unverified lifecycle caps the verdict (Phase 5).\n\n### Session work-record (append-only; Phase 5 renders from this, never composes fresh)\n\n| Field | Definition |\n|---|---|\n| `visitedSurfaces` | ordered list of `{path, action}`, appended after each Act step. Identity = normalized pathname (query\u002Fhash stripped, redirect followed to final URL); the list is a **chronological transition log**, not a deduped set — a reload is a repeat entry, not a new reach. Phase 5's \"N screens\" figure is `unique(pathname)` over this list, computed at render time |\n| `stepCount` | `{completed, cap}` — `cap` is this loop's configured budget (8–12, or an explicit override); `completed` is the number of Act steps actually executed |\n| `declaredFocus` | the `--focus` argument verbatim, or the literal string `\"none — exploratory\"` |\n| `originPolicyResult` | one of `same-origin-only` (default; nothing cross-origin attempted) \u002F `cross-origin-attempted-and-blocked` (the persona tried; the safety policy above refused) \u002F `n\u002Fa` |\n| `terminalReason` | **exhaustive, closed enum** — set once, when the loop stops: `goal-reached` \u002F `step-budget-exhausted` \u002F `abandonment-threshold-hit` \u002F `auth-wall-blocked` \u002F `tool-error` \u002F `safety-refusal` |\n| `authState` | `n\u002Fa-no-auth-encountered` \u002F `authenticated-via-bootstrap` \u002F `auth-wall-untested` (the last aligns with `authWallUntested` below) |\n| `lifecycle` | `{status, unchecked}` from the stateful mission checklist above — `verified` \u002F `partial` (+ the unchecked step ids) \u002F `not-applicable` |\n\n**`severity` is load-bearing, not a label** — it must be the literal string\n`\"P0\"`, `\"P1\"`, `\"P2\"` or `\"P3\"`, and it is the field the auto-correlator\n(Phase 6b) filters on to decide what reaches `persona_audit_correlations`.\nA finding that spells it anything else is silently uncorrelatable; the\ncorrelator reports `p0p1-shape-mismatch` when your declared counts and this\nfield disagree.\n\n### Special cases\n\n- **404 \u002F page-not-found** → 1 retry after 5s; if still 404, emit P0 \"Target URL unreachable\" and stop\n- **Login wall** → branches three ways, in this order:\n  1. **No bootstrap was ever configured for this app** → emit P3 \"App\n     requires login; test scope limited to public surface\", continue with\n     public pages only, and set `authWallUntested = true` for Phase 4 (and\n     the session work-record's `authState = 'auth-wall-untested'`) — this\n     run did **not** cover the app's primary authenticated surfaces, and\n     the report must say so even when it finds 0 P0s there.\n  2. **A bootstrap was configured, and this looks like the connect-time\n     race** (short-lived token, session has been running a while, a\n     `storageState` file exists on disk) → this is a rig problem, not\n     signal about the product. Attempt the escape hatch in\n     `references\u002Fauth-bootstrap.md` (re-seed, then inject the refreshed\n     `storageState` via `browser_evaluate` and reload). If it clears the\n     login wall, continue the run normally — do **not** emit a finding for\n     the recovery; the report's Auth coverage line may note the\n     mid-session refresh as context, but it is not a P0-P3 item.\n  3. **The escape hatch fails after a fresh re-seed** (or the app's\n     session is HttpOnly-cookie-based and unrecoverable in-session) → this\n     is a genuine setup regression. Emit **P1** \"Auth bootstrap did not\n     authenticate the session\" (misconfigured\u002Fexpired `storageState` —\n     `references\u002Fauth-bootstrap.md`).\n- **Page-load timeout** → retry once with viewport reset; if it still times out, emit P1 \"Slow initial load (>15s)\" and continue\n- **Visible JS errors \u002F console errors** → emit P1 or higher with the exact error text\n\n---\n\n## Phase 3b — Consistency Mode (deterministic, code-driven)\n\n> Triggered by `--mode consistency`. **This is a completely different\n> execution model from the exploratory loop above.** When `--mode consistency`\n> is set, the LLM does NOT drive the browser — you delegate to the\n> deterministic runner which owns Playwright directly. Skip Phases 1-3 and\n> Phases 4-6 above; consistency mode has its own flow below.\n\nAuthoritative spec: [docs\u002Freference\u002Fconsistency-contract.md](https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fdocs\u002Freference\u002Fconsistency-contract.md).\n\n### Step C1 — Validate inputs\n\nRequired: `--mode consistency` + `--canary \u003Cname>` + URL. If any missing,\nprint usage and STOP.\n\n### Step C2 — Delegate to the runner\n\n```bash\nnode scripts\u002Fpersona-consistency-run.mjs --canary CANARY_NAME --url \"https:\u002F\u002Fyour-app.example.com\"\n```\n\nOptional: `--out .persona-test\u002Fsessions\u002F$SID.json` to write the session artifact to a specific path.\n\nThe runner resolves the manifest + canary, drives `canary.journeySteps[]` with\nno LLM in the loop, diffs DOM vs network ground truth, and checks\n`expectedContradictions`. → `references\u002Fconsistency-runner-and-report.md` §What the runner does.\n\n### Step C3 — Read the verdict from the exit code (do NOT parse stdout)\n\n| Exit | Verdict | Action |\n|---|---|---|\n| `0` | healthy — canary expectations met | Report success; surface contradictions count + any pending candidates |\n| `2` | rig-broken — canary expected ≥N contradictions, got fewer | **Stop the pipeline.** Surface the failureReason from the ledger; investigate manifest drift or attribute regression before running again |\n| `3` | fatal-rig — manifest missing \u002F canary schema invalid \u002F Playwright disconnected | Surface failureReason; the rig itself needs fixing |\n| `4` | ledger-persist-failed — couldn't write session JSON | Surface stderr; disk full \u002F permission issue, distinct from rig findings |\n| `5` | playwright-missing | Suggest `npx playwright install chromium`; the runner emits this hint to stderr too |\n| `6` | app-error — a journey action threw (e.g. TimeoutError on click) | This is an APP regression, NOT a rig issue. Surface the failing step + selector from the ledger |\n\n### Step C4 — Report\n\nRender the `CONSISTENCY MODE REPORT` fence (Canary, URL, Verdict, Exit,\ncounts; FINDINGS sorted P0 first, tie-break by surfaceId; OVERALL). On a\n`broken` verdict, lead with the `⚠ RIG BROKEN` callout.\n→ `references\u002Fconsistency-runner-and-report.md` §Report fence — read before rendering it.\n\n### Step C5 — Skip the persona debrief\n\nConsistency mode does NOT produce a Phase 5b debrief. The exit code + the\nfindings fence is the entire report — rig output, not persona perception.\n\nFull grammar + manifest schema + canary schema + flow details:\n[references\u002Fconsistency-mode.md](references\u002Fconsistency-mode.md).\n\n---\n\n## Phase 4 — Severity Model\n\n| Code | Label | Rule |\n|---|---|---|\n| **P0** | BROKEN | Primary flow fails; user cannot complete their intent |\n| **P1** | DEGRADED | Flow completes but is confusing, slow, or missing clear feedback |\n| **P2** | COSMETIC | Visual \u002F layout \u002F polish issue; flow works |\n| **P3** | OBSERVATION | Not a bug — preference, suggestion, or informational note |\n\nConfidence threshold: ≥0.6 to report, ≥0.7 for P0, ≥0.8 when calling a\nrecurring P0 from history.\n\n**`authWallUntested` rule**: 0 P0s on such a run means \"0 P0s on the public\nshell\", not on the app — OVERALL reads `Needs work` at best, never `Ready for\nusers` (it is one conjunct of Phase 5's predicate).\n→ `references\u002Frun-loop-detail.md` §Phase 4 — read when the verdict is contested.\n\n---\n\n## Phase 5 — Structured Report\n\nEmit the report inside this fence. Sort findings P0 first, ties by\nconfidence descending:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n  PERSONA TEST REPORT\n  Persona: \u003Cpersona>\n  URL: \u003Curl>\n  Focus: \u003Cfocus or \"exploratory\">\n  Device: \u003Cpreset_name> \u003CWxH> (touch=\u003Cbool>, resolved-from=\u003Cdescription|explicit|fallback>)\n  Driver: \u003Cbrowser_driver> (\u003Cbrowser_status>) — \u003CN> steps — \u003Cduration>\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nFINDINGS\n────────────────────────────────────────────────────\n  [P\u003Cn>] \u003Ctitle> (confidence: \u003Cn>)\n     Element:  \u003Cselector>\n     Observed: \u003Cwhat happened>\n     Fix:      \u003Cspecific recommendation>\n  ...\n\nCOVERAGE (relative to this persona's own session — not an inventory scan)\n────────────────────────────────────────────────────\n  Reached:        \u002F  →  \u002Fcellar  →  \u002Fcellar\u002Fadd   (3 unique screens, 9\u002F12 steps)\n  Loop ended:     \u003CterminalReason>\n  Focus:          \u003CdeclaredFocus>\n  Origin:         \u003CoriginPolicyResult>\n  Auth:           \u003CauthState>\n  Lifecycle:      \u003Cverified | partial (unchecked: \u003Cstep ids>) | not-applicable>\n\n  Not a surface-complete scan: this persona explored as it naturally would,\n  not exhaustively. For surface-complete coverage, run \u002Fnav-audit or\n  \u002Fclick-test against the same URL.\n\nOVERALL: \u003CReady for users | Needs work | Blocked>\n  Reason: \u003Cone sentence>\n```\n\nEvery field in the COVERAGE block is rendered directly from the session\nwork-record (Phase 3) — never recomposed here. `Reached` is `unique(pathname)`\nover `visitedSurfaces`; the step fraction is `stepCount.completed\u002Fcap`.\n\n**`Ready for users` requires a composed eligibility predicate, not a single\ncheck** — a clean run (no P0\u002FP1 findings) AND **every one** of:\n\n```\nterminalReason      === 'goal-reached'\nauthState           !== 'auth-wall-untested'\noriginPolicyResult  !== 'cross-origin-attempted-and-blocked'\nlifecycle.status    in ('verified', 'not-applicable')\n```\n\n**A reached goal with an unverified lifecycle is not a clean pass.** When\n`lifecycle.status` is `partial` (and nothing else fails), OVERALL is\n`Needs work` and the Reason line opens `Goal reached — lifecycle unverified`,\nthen lists the unchecked steps. The cap is enforced in code too:\n`record-persona-session` stores `Needs work` for a claimed `Ready for users`\nwhose payload fails any conjunct — including a MISSING `lifecycle`, which is\nunreported, not verified — and says so in the response's `verdictCap`.\n\nAny failing conjunct → OVERALL `Needs work`, and the Reason line names\n**every** failing conjunct. → `references\u002Frun-loop-detail.md` §Phase 5 — why\n`terminalReason` alone is not enough, plus a worked Reason line.\n\nIf `audit_link = true` and Phase 0d candidates match the persona findings,\nappend an **AUDIT CORRELATIONS** section mapping each P0\u002FP1 to a possible\nroot-cause audit finding. Keyword-matched — tag as \"verify before assuming\ncausation\".\n\nFull example output (report + debrief): `examples\u002Freport-and-debrief.md`.\n\n---\n\n## Phase 5b — Persona Debrief\n\nAfter the structured report, emit a **Persona Debrief** — 400–700 words\nin first person as the persona. Product discovery artefact, not a bug list.\nGrounded in session observations, not generic user opinions.\n\nFull tone rules, structure, and wrapper: `references\u002Fpersona-debrief-format.md`.\n\n---\n\n## Phase 6 — Save Session to Memory\n\nSkip if `memory_enabled = false`. Output `[Session not saved — memory disabled]` and stop.\n\nOmit `sessionId` — the CLI mints a collision-resistant one and returns it as\n`sessionKey`; pass `sessionId` only to re-post an existing session.\n\nRecord the session + trigger secondary persona stats update in one call:\n\n```bash\nnode scripts\u002Fcross-skill.mjs record-persona-session --json '{\n  \"persona\": \"\u003Cpersona>\",\n  \"url\": \"\u003Curl>\",\n  \"focus\": \"\u003Cfocus or null>\",\n  \"browserTool\": \"\u003Cbrowser_driver> (\u003Cok|degraded|blocked>)\",\n  \"stepsTaken\": \u003CN>,\n  \"verdict\": \"\u003Cverdict>\",\n  \"lifecycle\": {\"status\": \"\u003Cverified|partial|not-applicable>\", \"unchecked\": [\u003Cstep ids>]},\n  \"terminalReason\": \"\u003CterminalReason>\",\n  \"authState\": \"\u003CauthState>\",\n  \"originPolicyResult\": \"\u003CoriginPolicyResult>\",\n  \"p0Count\": \u003Cn>, \"p1Count\": \u003Cn>, \"p2Count\": \u003Cn>, \"p3Count\": \u003Cn>,\n  \"avgConfidence\": \u003C0-1>,\n  \"findings\": \u003CJSON array>,\n  \"reportMd\": \"\u003Creport text>\",\n  \"debriefMd\": \"\u003Cdebrief text>\",\n  \"commitSha\": \"\u003Cauto-detected if omitted>\",\n  \"deploymentId\": \"\u003Coptional>\",\n  \"repoName\": \"\u003Crepo_name or null>\",\n  \"personaId\": \"\u003Cpersona_id or null>\",\n  \"clickPath\": \u003CJSON array — the path you walked; see \"Building clickPath\" below>\n}'\n```\n\n### Building `clickPath` (reachability evidence for `\u002Fnav-audit`)\n\nOne entry per Act step that *moved or activated something* (click \u002F navigate \u002F\nopen \u002F submit):\n\n```json\n{ \"step\": \u003C1-based step number>,\n  \"action\": \"click|navigate|type|fill|select|hover|scroll|press|submit|wait\",\n  \"url\": \"\u003Cthe page URL AFTER the step settled>\",\n  \"targetText\": \"\u003Cvisible label of the control you acted on, ≤80 chars, or null>\" }\n```\n\n`targetText` is the control's visible label — typed input values stay out of\nthe payload. Omit the field (not `[]`) if you tracked no path.\n→ `references\u002Fsession-save-detail.md` §clickPath edges.\n\n**Response** — keep `sessionId` (the row uuid; later phases pass it as\n`personaSessionId`), not `sessionKey`. `statsUpdated: false` means the\npersona's `last_tested_at`\u002F`last_verdict` were not updated — report\n`statsReason` (`no-persona-id` \u002F `stats-write-failed` \u002F `persona-not-found`);\nnothing back-fills them. → `references\u002Fsession-save-detail.md` §Response fields.\n\nIf `verdictCap` is present, the store did NOT record the verdict you claimed:\n`{\"claimed\": \"Ready for users\", \"stored\": \"Needs work\", \"blockers\": [...],\n\"label\": ...}`. Correct the report's OVERALL line to match — the report and\nthe stored session must never disagree.\n\n---\n\n## Phase 6b — Verify the Correlation Summary (automatic — do NOT re-emit)\n\n`record-persona-session` (Phase 6) runs the correlator itself and writes the\n`persona_audit_correlations` rows. **Do not call `record-correlation` per\nfinding** — it exists only for manual repair (below).\n→ `references\u002Fsession-save-detail.md` §Phase 6b.\n\nYour only job this phase: **read `correlationSummary` from the Phase 6\nresponse and report it** (echo the counts — `candidates`, `route`, `fuzzy`,\n`missed`, `skippedExisting`, `malformed`, `writeFailed` — into the report).\n→ `references\u002Fsession-save-detail.md` §correlationSummary — read when a field's meaning is unclear.\n\n`attempted: false` means the correlator didn't run at all — `reason` says why\n(`disabled-by-flag`, `no-repo-identity`, `no-p0p1-findings`,\n`p0p1-shape-mismatch`, `session-write-failed`). **`p0p1-shape-mismatch` is\nnever benign**: you declared `p0Count`\u002F`p1Count` above zero but not one entry\nin `findings` carried a `severity` of `P0`\u002F`P1`, so nothing could be\ncorrelated. Fix the payload and re-post — every finding needs `severity`\n(see Phase 3), not a differently-named field. `attempted: true` with a `reason` still set\n(`no-candidate-runs`, `candidate-read-failed`, `existence-check-failed`) means\nit tried but couldn't compare against anything real — in BOTH cases, **zero\ncorrelation rows were written**, which is correct (an empty candidate set is\nnot evidence of an audit miss — never treat it as one). `malformed > 0`\n(findings missing `element`\u002F`observed`) were quarantined, correctly.\n`writeFailed > 0` is the one state\nworth flagging loudly in the debrief (a real DB write failed for some\nfindings; already logged to stderr by the correlator).\n\n### Manual repair — only for genuine corrections\n\nUse `record-correlation` **only** to:\n1. **Repair a false auto-emitted `audit_missed`** into a real match (supplying\n   a real `auditFindingId` retires the stale row automatically).\n2. **Emit `audit_false_positive` or `severity_overstated`** — human-judgment\n   types, never auto-emitted.\n\nCompute `personaFindingHash()` the same way the auto-correlator does\n(`references\u002Faudit-correlation.md`) so the repair matches the row it corrects.\n**When one of the two repair cases applies, run exactly one command per\nfinding being corrected** — payload via a Write-tool file under `.audit\u002F`\npiped on `--stdin`, not a shell-quoted `--json` string or heredoc\n(`references\u002Fshell-safe-payloads.md` §Manual correlation repair):\n\n1. Write `.audit\u002Fcorrelation-repair.json` with the Write tool:\n   ```json\n   {\n     \"personaSessionId\": \"the sessionId from Phase 6\",\n     \"personaFindingHash\": \"personaFindingHash() of the persona finding\",\n     \"personaSeverity\": \"P0|P1\",\n     \"auditFindingId\": \"the matching audit_findings.id, or omit if none\",\n     \"auditRunId\": \"the matched run id, or omit\",\n     \"correlationType\": \"confirmed_hit | audit_missed | severity_understated | audit_false_positive | severity_overstated\",\n     \"matchScore\": 0.0,\n     \"matchRationale\": \"one line\"\n   }\n   ```\n2. Pipe it in:\n   ```bash\n   cat .audit\u002Fcorrelation-repair.json | node scripts\u002Fcross-skill.mjs record-correlation --stdin\n   ```\n\nIdempotent (dedupes on session + hash + audit finding). Do **not** loop this\nover every P0\u002FP1 finding. → `references\u002Fsession-save-detail.md` §Manual repair,\nand `references\u002Faudit-correlation.md` for the classification rules.\n\n---\n\n## Phase 6c — Session History Readback\n\nAfter saving, surface patterns across prior sessions: recent runs, recurring\nissues (≥2 occurrences), persistent P0s (via the `persistent_p0s` view).\n\nSkip silently when Supabase vars are not set.\n\nFull query shapes + output format: `references\u002Fsession-history.md`.\n\n---\n\n## Phase 7 — Pair Mode (--pair)\n\nTriggered by `--pair \"\u003Cp1>\" \"\u003Cp2>\"` anywhere in `$ARGUMENTS`. Skip Phases\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=default -->\n_This site: `subcommand` — absence of `--pair` is a normal single-persona run — do not infer pair mode from two persona names being mentioned._\n\n0b–6c above and follow the flow below.\n\nTwo opposed personas surface disjoint findings. → `references\u002Fpair-mode.md` §Why.\n\n### Step P1 — Parse pair arguments\n\nParse from `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — both personas must be quoted after `--pair`; one missing prints usage and stops._\n\n1. **persona_a** — first quoted string after `--pair`\n2. **persona_b** — second quoted string\n3. **url** — URL in remaining args (or `PERSONA_TEST_APP_URL` env)\n4. **focus** — anything after the URL\n\nRequired: both personas + url. If missing, output usage and STOP.\n\n### Step P2 — Run persona A end-to-end\n\nRun **Phases 0c → 6c** for persona A as if it were a solo run (its own Phase 1a\ndevice resolution). Capture:\n- `report_a` (Phase 5 report text)\n- `findings_a` (the structured findings array used in Phase 5)\n- `verdict_a`\n- `session_id_a` (from Phase 6, may be null if memory disabled)\n\nThen close the browser session (`browser_close`) — persona B gets a fresh\ncontext, including a fresh cache-bust in Phase 1b. **Do not skip the\ncache-bust for persona B** — it's not redundant; the browser context was\ntorn down.\n\n### Step P3 — Run persona B end-to-end\n\nSame flow for persona B. Capture `report_b`, `findings_b`, `verdict_b`,\n`session_id_b`.\n\n### Step P4 — Diff the findings\n\nTwo findings overlap if **either**:\n- Same `element` selector AND same `severity`, OR\n- Jaccard similarity of `observed` text ≥ 0.6 (token-level after lowercasing\n  + stripping punctuation; ignore stopwords)\n\nClassify each finding into:\n- **CONSENSUS** — overlapping pair from A and B (high signal — both saw it)\n- **A-ONLY** — finding from A with no overlap in B (coverage signal — A's expertise caught it)\n- **B-ONLY** — finding from B with no overlap in A (coverage signal — B's expertise caught it)\n\n### Step P5 — Emit pair report\n\nPrint both solo reports in full, then the `PAIR DIFF` fence (both devices and\nverdicts in the header; CONSENSUS \u002F A-ONLY \u002F B-ONLY; overlap rate\n`consensus \u002F (consensus + a_only + b_only)`). → `references\u002Fpair-mode.md`\n§Step P5 — read before rendering it.\n\nOVERALL uses the solo vocabulary, and is the worse of the two solo verdicts\n(`Blocked` > `Needs work` > `Ready for users`): the pair is ready for users\nonly when both personas' runs independently are — including each one's\nlifecycle conjunct.\n\n### Step P6 — Skip the secondary debrief\n\nEach solo run already produced a Phase 5b debrief; do not write a pair debrief.\n\n### Step P7 — Session linkage\n\nWhen both solo runs were saved (memory enabled), record the pairing. The ids\nare each Phase 6 response's `sessionId` (the row uuid), **not** `sessionKey`:\n\n```bash\nnode scripts\u002Fcross-skill.mjs link-persona-pair --json '{\n  \"sessionA\": \"\u003CsessionId from persona A's Phase 6>\",\n  \"sessionB\": \"\u003CsessionId from persona B's Phase 6>\",\n  \"consensusCount\": \u003Cn>,\n  \"aOnlyCount\": \u003Cn>,\n  \"bOnlyCount\": \u003Cn>,\n  \"overlapRate\": \u003C0-1>\n}'\n```\n\nSend the counts you printed in Step P5 (`overlapRate` is optional and refused\nif it disagrees). A refusal exits 1 naming `PARENT_NOT_FOUND` or\n`PARENT_NOT_OWNED`. With the cloud store off (`\"cloud\": false, \"pairId\": null`)\nsay in the report that the pairing was not persisted.\n→ `references\u002Fpair-mode.md` §Step P7.\n\n---\n\n## Reminders\n\n- **You are the persona** — every click and judgement from their perspective\n- **Be specific** — \"button looks bad\" is useless; \"CTA [7] has no hover state on mobile 390px\" is actionable\n- **The verdict matters** — it drives whether the user ships or fixes\n\n---\n\n## Reference files\n\nThis skill's canonical flow is above. The files below cover specialised\nsituations — read them only when the trigger applies.\n\n| File | Summary | Read when |\n|---|---|---|\n| `references\u002Finput-acquisition.md` | Where a skill's arguments come from on any host, and what to do when there are none. | Reading $ARGUMENTS on a host that does not substitute it, or deciding what empty input means at a site. |\n| `references\u002Faudit-correlation.md` | Pre-test audit enrichment + post-test persona↔audit correlation emission — full rules. | `audit_link = true` AND (Phase 0d fetches audit candidates OR Phase 6b's manual-repair path is needed). |\n| `references\u002Fbrowser-tool-detection.md` | The browser-driver contract — capabilities, driver table, selection order, minimum sets, degraded\u002Fblocked evidence. | Phase 1 tool selection fails on first try, OR the user is on Windows and Playwright MCP tools aren't appearing. |\n| `references\u002Fauth-bootstrap.md` | Sanctioned auth-gated exploratory-testing pattern via MCP-server storageState, plus its connect-time-race escape hatch. | Phase 1 detects a login-gated target, OR Phase 3 hits a login wall (bootstrap configured or not). |\n| `references\u002Fverification-discipline.md` | Verification discipline — pinned citations, figure provenance, two-direction proof, attribution, consumer-side checks. | Phase 3 Record step \u002F Phase 5 — rendering the COVERAGE block, to apply §7's reciprocal warning (it renders only from the session work-record). |\n| `references\u002Fconsistency-mode.md` | Full consistency-mode grammar, manifest schema, canary schema, runner exit codes, contradiction kinds. | Phase 3b runs (i.e., `--mode consistency` was passed) and you need the full grammar reference; OR the user asks how the rig decides severity \u002F coercion \u002F negative-space. |\n| `references\u002Fconsistency-runner-and-report.md` | Consistency-mode runner steps (Step C2) and the Step C4 report fence, including the RIG BROKEN callout. | Phase 3b — about to run the consistency runner (Step C2) or render its report (Step C4). |\n| `references\u002Fpersona-debrief-format.md` | Full persona debrief generation rules, tone guide, and output wrapper. | About to write the Phase 5b debrief. |\n| `references\u002Fsession-history.md` | Post-session history readback — recurring-issue surface + cross-session pattern detection. | Phase 6c runs AND Supabase is configured. |\n| `references\u002Finterop.md` | How persona-test interacts with \u002Fship, \u002Fplan, and \u002Faudit-code\u002Faudit-plan — integration contracts. | User asks about cross-skill effects, OR a sibling skill needs to reference persona-test data. |\n| `references\u002Fshell-safe-payloads.md` | Why ADD and record-correlation payloads go through a Write-tool file on stdin, never a shell-quoted --json string. | Running `add` or a Phase 6b manual repair and tempted to inline the strings, OR asked why a file is used. |\n| `references\u002Fdevice-profile.md` | Phase 1a detail — device prep contract shape, mobile tag semantics, viewport-only limits, MCP launch flags, pair mode. | Phase 1a — the prep contract is unclear, a touch\u002FUA\u002FDPR bug is suspected, OR a pair run resolves two devices. |\n| `references\u002Frun-loop-detail.md` | Why the run-loop rules exist — cloud probe, auth at connect, cache-bust, stateful missions, Ready-for-users. | The cloud probe disagrees with env, the target is login-gated, OR a capped verdict is contested. |\n| `references\u002Fsession-save-detail.md` | Phase 6\u002F6b detail — session-id minting, clickPath edges, response fields, correlationSummary, manual repair. | Phase 6\u002F6b — a response field (statsReason, correlationSummary count) is unclear, OR a manual repair is needed. |\n| `references\u002Fpair-mode.md` | Pair mode detail — why opposed personas, the Step P5 PAIR DIFF fence, overlap interpretation, Step P7 responses. | `--pair` was passed — before rendering Step P5, OR Step P7 returned a refusal. |\n| `examples\u002Freport-and-debrief.md` | Sample full persona-test output — structured report + debrief fences and example content. | About to emit Phase 5 + 5b output and unsure of the exact fence format. |\n| `references\u002Fworktree-preflight.md` | Worktree preflight remedy — the skills:hydrate npm script to add when a linked worktree lacks the tooling tree. | A command dies with `MODULE_NOT_FOUND` in a linked worktree AND this repo has no `skills:hydrate` script. |\n\n— end of \u002Fpersona-test —\n","> **Worktree preflight** — in a linked git worktree the synced tooling tree\n> `scripts\u002F.claude-skills\u002F` is absent — it is gitignored, so `git worktree add`\n> does not populate it, and every command below that uses it dies on a bare\n> `MODULE_NOT_FOUND`. Run `npm run skills:hydrate` first. If this repo defines\n> no such script, `references\u002Fworktree-preflight.md` holds the entry to add — ask\n> the user first, since it edits their `package.json`.\n\n## Usage\n\n```\nUsage:\n  \u002Fpersona-test list [url]                                                       — show all personas for an app\n  \u002Fpersona-test add \"\u003Cname>\" \"\u003Cdescription>\" \u003Curl> [app name]                    — register a new persona\n  \u002Fpersona-test \"\u003Cpersona or name>\" \u003Curl> [focus] [--device \u003Cpreset>]            — run an exploratory test (device auto-resolved from persona description)\n  \u002Fpersona-test --pair \"\u003Cp1>\" \"\u003Cp2>\" \u003Curl> [focus] [--device \u003Cpreset>]           — pair mode (--device overrides both personas)\n  \u002Fpersona-test --mode consistency --canary \u003Cname> \u003Curl>                         — deterministic consistency canary (code-driven Playwright)\nDevice presets: desktop (default fallback) | desktop-large | tablet | mobile | mobile-small\nExamples:\n  \u002Fpersona-test list https:\u002F\u002Fmyapp.railway.app\n  \u002Fpersona-test add \"Pieter\" \"wine enthusiast, 40s, drinks daily, mobile-first\" https:\u002F\u002Fmyapp.railway.app \"Wine Cellar App\"\n  \u002Fpersona-test \"Pieter\" https:\u002F\u002Fmyapp.railway.app \"adding a bottle\"\n  \u002Fpersona-test \"first-time user on mobile\" https:\u002F\u002Fmyapp.railway.app\n  \u002Fpersona-test \"Pieter\" https:\u002F\u002Fmyapp.railway.app --device mobile-small      — override resolved device\n  \u002Fpersona-test --pair \"Elena (sommelier)\" \"Martha (newer drinker)\" https:\u002F\u002Fmyapp.railway.app \"browsing the cellar\"\n  \u002Fpersona-test --mode consistency --canary oliver-infeasible-reorg http:\u002F\u002Flocalhost:3000\n```\n\n# Persona-Driven Browser Testing\n\n> **This skill's last line is an end marker naming the skill between em-dashes, after\n> the Reference files table.** If that last line is not in your context (context\n> compaction keeps only the start of a skill), re-read\n> `.claude\u002Fskills\u002Fpersona-test\u002FSKILL.md` in full before continuing — the later steps still apply.\n\nRun an exploratory browser test with persona tracking. Check `$ARGUMENTS`\nfirst to pick the sub-command.\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n\n**Where `$ARGUMENTS` comes from** — orchestrator-supplied input first, else\nthe host's verbatim invocation suffix, else the span of the user's **current**\nmessage naming this skill or its subject. Never inferred from surrounding\nconversation. This site is `subcommand`; on empty input, print usage and stop — a persona and a URL are both required, and there is no safe default target to browse.\nFull contract: `references\u002Finput-acquisition.md`.\n\n\n---\n\n## Phase 0 — Route the Command\n\nRead the first word of `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — no first word means no sub-command; fall through to the test-argument parse, which stops with usage if a persona or URL is missing._\n\n\n- `list` → **Sub-command: LIST**\n- `add` → **Sub-command: ADD**\n- contains `--mode consistency` anywhere → **Phase 3b: Consistency Mode** (deterministic canary run; skips Phases 0b–3a entirely — it needs only `--canary \u003Cname>` + URL, no persona resolution)\n- contains `--pair` anywhere → **Sub-command: PAIR** (see Phase 7 at the end)\n- otherwise → **Phase 0b: Parse Test Arguments** (normal test run)\n\n---\n\n## Sub-command: LIST\n\n**Usage**: `list [url]`\n\nResolve the URL in order: positional argument → `PERSONA_TEST_APP_URL` env →\nask the user.\n\n**Validate the URL before it reaches a shell.** It is user- or env-derived, and\ndouble quotes do **not** neutralise `$(…)` or backticks — the same defect class\nas the ADD payload below. Parse it with `new URL(value)` and reject it outright\nif the parse fails or the string contains any of `` $ ` ; | & \u003C > ( ) `` or a\nnewline. Only a value that survived that check is substituted below:\n\n```bash\nnode scripts\u002Fcross-skill.mjs list-personas --url \"\u003Cvalidated url>\"\n```\n\nResponse `{\"ok\": true, \"cloud\": true|false, \"rows\": [\u002F* persona_dashboard rows *\u002F]}`\n(graceful no-op when cloud is off). Render a `PERSONA ROSTER — \u003Capp_name or url>`\ntable (NAME · LAST TESTED · VERDICT · SESSIONS) sorted **never-tested first,\nthen oldest last-tested**, then one `SUGGESTION:` line naming who to run next.\n\nSTOP — do not proceed to the test phases.\n\n---\n\n## Sub-command: ADD\n\n**Usage**: `add \"\u003Cname>\" \"\u003Cdescription>\" \u003Curl> [app name]`\n\nParse from `$ARGUMENTS` after `add`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — name, description and url are required; a missing one prints usage and stops rather than registering a partial persona._\n\n1. **name** — first quoted string\n2. **description** — second quoted string\n3. **url** — URL following the quoted strings\n4. **app_name** — everything after the URL (optional)\n\nIf name, description, or url is missing, output usage and STOP.\n\nUpsert (idempotent on `name + app_url`). **Write the payload to a file with the\neditor, then pass the file — never interpolate the user's strings into a\nsingle-quoted shell argument:**\n\n```bash\n# 1. Write .audit\u002Fadd-persona.json with the Write tool (NOT a shell heredoc):\n#    {\"name\":\"…\",\"description\":\"…\",\"appUrl\":\"…\",\"appName\":null}\n# 2. Pipe it on stdin — a pipe passes bytes, never shell-parsed text, and\n#    (unlike `\u003C` redirection, which PowerShell does not support) `cat | …`\n#    parses the same way in bash and PowerShell (`cat` is a built-in\n#    PowerShell alias for `Get-Content`):\ncat .audit\u002Fadd-persona.json | node scripts\u002Fcross-skill.mjs add-persona --stdin\n```\n\nDo not substitute `--name`\u002F`--description`\u002F`--url` flags — they put the free\ntext back on the command line. → `references\u002Fshell-safe-payloads.md` §ADD — read\nwhen asked why a file, or tempted to inline the strings.\n\nResponse `{\"ok\": true, \"cloud\": ..., \"personaId\": ..., \"existed\": bool}`.\nReport success with `personaId`. STOP.\n\n---\n\n## Phase 0b — Parse Test Arguments (normal test run)\n\nParse `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — persona and url are required; never substitute a previously-tested URL from earlier in the conversation._\n\n1. **persona_input** — first quoted string or first unparsed token\n2. **url** — URL in the remaining args (or `PERSONA_TEST_APP_URL` env)\n3. **focus** — any remaining text after the URL (strip out flag tokens before assigning)\n4. **device_override** — value of `--device \u003Cpreset>` if present (must be one of:\n   `desktop`, `desktop-large`, `tablet`, `mobile`, `mobile-small`). Unknown\n   preset → fail with usage. Drives Phase 1a's explicit-override branch.\n\nRequired: `persona_input` + `url`. If either is missing, output usage and STOP.\n\n### Pre-flight environment check\n\n| Env var | Purpose |\n|---|---|\n| `AUDIT_DB_URL` | Cloud store on — saves sessions, reads history, and (with a repo name) cross-references audit findings. One Postgres store backs every feature post-M4. |\n| `PERSONA_TEST_REPO_NAME` | Enables audit-loop cross-reference (`audit_link = true`) |\n\nDo not read `AUDIT_DB_URL` from the session's `process.env` — the consumer's\n`.env` is not loaded there, so it reads unset and the run silently goes\nstateless. Probe through the CLI, from the consumer-repo root:\n\n```bash\nnode scripts\u002Fcross-skill.mjs whoami\n```\n\n→ `references\u002Frun-loop-detail.md` §Pre-flight — read when the probe and the env disagree.\n\nSet `memory_enabled = .cloud` from the probe's JSON output. When `cloud`\nis false the skill runs in \"stateless\" mode — tests complete but nothing is\nsaved or cross-referenced. `audit_link` is NOT computed here — it needs\n`repo_name`, which isn't resolved until Phase 0c below; see there.\n\n---\n\n## Phase 0c — Resolve Persona\n\nIf `persona_input` matches a registered persona name (for this `url`):\n- `persona = matched.description`, `persona_id = matched.id`, `persona_name = matched.name`, `repo_name = matched.repo_name`\n\nOtherwise treat `persona_input` as an ad-hoc persona description;\n`persona_id = null`.\n\nIf `repo_name` is not on the persona, detect from `PERSONA_TEST_REPO_NAME` env,\nor `git remote get-url origin`, or leave null.\n\nNow that `repo_name` is resolved (or confirmed null), compute\n`audit_link = memory_enabled && repo_name` — cross-referencing needs both\ncloud storage AND a resolved repo identity to look findings up against.\n\n---\n\n## Phase 0d — Audit-Loop Pre-Test Enrichment\n\nSkip if `audit_link = false`. When on, fetch recent HIGH + MEDIUM audit\nfindings (with `id` + `run_id` for Phase 6b correlations) and add a\n**Known Code Fragilities** section to the persona mental model in Phase 2.\n\nFull rules + query shape: `references\u002Faudit-correlation.md`.\n\n---\n\n## Phase 1 — Detect Browser Tool\n\nResolve the driver through the **one detection oracle** —\n`references\u002Fbrowser-tool-detection.md`. Do not restate its ladder here; this\nstep only declares what persona-test needs from it.\n\n**Minimum capability sets** (§4 of the oracle):\n\n- **Full journey**: `navigate`, `click`, `type`, `evaluate`, `screenshot`,\n  `wait`, `currentUrl`.\n- **Read-only degraded**: `readText` alone (it takes a URL, so it needs no\n  `navigate`). Below that → `blocked`.\n\nSet `browser_driver` to the selected driver id (`playwright-mcp`,\n`copilot-browser`, `brightdata`, `static-fetch`) and `browser_status` to `ok`,\n`degraded` or `blocked`, then keep both for the whole session — never mix\ndriver families mid-run.\n\n**A `degraded` run is reported as degraded.** Name the missing capabilities,\nprint the `[DEGRADED MODE]` banner at the top of the report, and mark every\ninteraction, flow and state-change stage **not-run** rather than passed. A\npartial capture degrades to `unverified`; it never becomes \"verified \u002F 0\nfindings\".\n\nFull contract — capability vocabulary, driver table, selection order, status\nevidence, Windows spawn caveats: `references\u002Fbrowser-tool-detection.md`.\n\n**Login-gated target** (primary surfaces behind login): first check whether\nthe MCP session is already authenticated; if not and this is a repeat\nsession, ask whether a `--storage-state` bootstrap exists. Seed it **before\nthe MCP server connects** — `.mcp.json` is read once, so a mid-session seed is\nnever re-read. → `references\u002Frun-loop-detail.md` §Phase 1 + `references\u002Fauth-bootstrap.md`\n— read when the target is login-gated.\n\n---\n\n## Phase 1a — Device Profile Resolution (required)\n\nSkip ONLY when `browser_status = \"degraded\"` and the driver has no viewport concept.\nOtherwise, this is non-negotiable — the LLM does not pick the device.\n\n### Step 1a.1 — Get the device contract (required)\n\nRun from the consumer-repo root:\n\n```bash\nnode scripts\u002Flib\u002Fdevice-presets.mjs prep \"the persona's description, or ad-hoc persona_input text\"\n```\n\nAdd `--device PRESET` only when `$ARGUMENTS` contained an explicit override preset (Phase 0b item 4).\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=default -->\n_This site: `subcommand` — no explicit `--device` means the device is resolved from the persona description, never guessed from context._\n\nThe CLI returns a JSON contract (`device`, `expectedFirstMcpCall`,\n`personaMentalModelTags`, `logLine`). **Echo the `logLine` verbatim to stderr**\n— it is the audit trail of which preset was applied.\n\n### Step 1a.2 — Execute `expectedFirstMcpCall` verbatim\n\nCall `browser_resize` with the args from `expectedFirstMcpCall.args` —\n**before Phase 1b's first `browser_navigate`**. Do not modify the args;\ndo not pick your own dimensions. If the contract said `{width: 390,\nheight: 844}`, that's what you call.\n\n### Step 1a.3 — Apply `personaMentalModelTags` to Phase 2\n\nWhen `device.isMobile === true`, apply the tags to Phase 2's mental model\n**silently** — they shape Reflect scoring, never the Phase 5b persona voice.\n\n→ `references\u002Fdevice-profile.md` — read when you need the full contract shape,\nthe tag semantics, what viewport-only emulation does not cover (UA, touch\nevents, DPR — use `--mode consistency` for those), or the pair-mode device rule.\n\n---\n\n## Phase 1b — Service-worker cache-bust (MANDATORY for own apps)\n\nService workers silently serve stale bundles, so a deployed fix reads as \"not\ndeployed\". → `references\u002Frun-loop-detail.md` §Phase 1b.\n\nSkip when `browser_status = \"degraded\"` (no `evaluate` capability). Skip when\nthe URL is a static-hosted page with no service worker (`*.github.io`, etc.).\nOtherwise, **run this before Phase 2**:\n\n```js\n\u002F\u002F browser_evaluate\n(async () => {\n  const regs = await navigator.serviceWorker?.getRegistrations() ?? [];\n  await Promise.all(regs.map(r => r.unregister().catch(() => {})));\n  const keys = await caches?.keys() ?? [];\n  await Promise.all(keys.map(k => caches.delete(k).catch(() => {})));\n  return { unregistered: regs.length, cachesDeleted: keys.length };\n})()\n```\n\nThen `browser_navigate({url})` again to force a fresh fetch. If the evaluate\nreturns `{unregistered: 0, cachesDeleted: 0}`, the page had no SW — proceed\nwithout the reload. Log the result one-line:\n`[cache-bust] unregistered \u003Cn> SW, cleared \u003Cn> caches`.\n\n**Don't** treat a non-zero `cachesDeleted` as a finding — caches are normal.\nThe finding-worthy event is when a fix doesn't appear after cache-bust;\nthat's a real deploy failure, not a caching artefact.\n\n---\n\n## Phase 2 — Build the Persona's Mental Model\n\nBefore driving, articulate the persona's profile in 5 dimensions:\n\n| Dimension | Prompt |\n|---|---|\n| **Background** | Age range, tech comfort, relevant domain knowledge, attitudes |\n| **Intent** | What are they trying to accomplish? What success looks like to them |\n| **First actions** | The 3 things they'd naturally try within 30 seconds of landing |\n| **Patience budget** | Low (phone, distracted) \u002F Medium (desktop, curious) \u002F High (research mode) |\n| **Abandonment threshold** | What would make them close the tab \u002F uninstall |\n\nIf `audit_link = true` and Phase 0d returned candidates, append the\n**Known Code Fragilities** list (silently sharpens Reflect — do not leak\nto the persona's \"voice\").\n\n---\n\n## Phase 3 — Safety Policy + Plan→Act→Reflect Loop\n\n### Safety policy (origin boundary)\n\n- **Never navigate away** from the target hostname\n- **Never submit real payment info, real credentials, or PII**\n- **Destructive actions** (delete accounts, delete data) require a fake\n  test-only context; if unavailable, SKIP the action and log it as \"deferred\"\n- Always call `browser_close` at the end, even if the session aborts\n\n### Exploration loop (8–12 steps)\n\nEach step is **Plan → Act → Reflect → Record**:\n\n1. **Plan** — one sentence: \"This persona would next try X because Y.\"\n2. **Act** — take the action (click, type, navigate); screenshot immediately after.\n3. **Reflect** — answer:\n   - Did the observed state match the expectation? (Yes \u002F No \u002F Partial)\n   - Does anything visible suggest a P0–P3 finding? (cite the element)\n   - What does this persona try next?\n4. **Record** — append to the session work-record below. This is not\n   optional bookkeeping: Phase 5's COVERAGE block renders **only** from this\n   record, never composed fresh at report time (`references\u002Fverification-discipline.md`\n   §7's reciprocal warning).\n\nRecord a finding only when confidence ≥0.6. Below that, note it as\n\"uncertain — did not report\". Every finding needs `element`, `observed`,\n`fix`, `severity`, `confidence` — and `step`, the 1-based Act step it was\nobserved on. `step` is how the auto-correlator (Phase 6b) finds the page URL\nthe finding happened on (via `clickPath`) and compares it with the audited\nfile that serves that page; a finding without it correlates on its text alone.\n\n### Stateful mission checklist (REQUIRED when the goal changes state)\n\nA goal that **creates, submits, approves, completes, or selects an item and\nhands it to another screen** is a stateful mission: reaching the goal is not\nverifying it. → `references\u002Frun-loop-detail.md` §Phase 3 — the incident behind this.\n\nFor a stateful mission, verify each of these six steps, by screenshot, and\nrecord which you could not:\n\n| Step id | Verify |\n|---|---|\n| `initial-state` | The state before acting — the queue\u002Flist\u002Fcount as it stands |\n| `selected-identity` | The immutable identity of what was selected (id, name, row) — note it before acting |\n| `post-boundary-state` | After a rerender or a navigation boundary, the SAME identity is still the one in play |\n| `command-outcome` | The command's own outcome — the success state, not just the absence of an error |\n| `return-to-origin` | Back at the origin screen, it reflects the change |\n| `retained-history` | History\u002Fprogress survives: completed items stay visible as completed, counts agree |\n\nRecord the outcome as the work-record's `lifecycle`:\n`{\"status\": \"verified\" | \"partial\" | \"not-applicable\", \"unchecked\": [\u003Cstep ids>]}` —\n`verified` (all six checked, `unchecked: []`), `partial` (at least one not\nchecked — list exactly which), or `not-applicable` (the goal changes no state:\nreading, browsing, searching). Spend step budget on the checklist before\nexploring sideways — an unverified lifecycle caps the verdict (Phase 5).\n\n### Session work-record (append-only; Phase 5 renders from this, never composes fresh)\n\n| Field | Definition |\n|---|---|\n| `visitedSurfaces` | ordered list of `{path, action}`, appended after each Act step. Identity = normalized pathname (query\u002Fhash stripped, redirect followed to final URL); the list is a **chronological transition log**, not a deduped set — a reload is a repeat entry, not a new reach. Phase 5's \"N screens\" figure is `unique(pathname)` over this list, computed at render time |\n| `stepCount` | `{completed, cap}` — `cap` is this loop's configured budget (8–12, or an explicit override); `completed` is the number of Act steps actually executed |\n| `declaredFocus` | the `--focus` argument verbatim, or the literal string `\"none — exploratory\"` |\n| `originPolicyResult` | one of `same-origin-only` (default; nothing cross-origin attempted) \u002F `cross-origin-attempted-and-blocked` (the persona tried; the safety policy above refused) \u002F `n\u002Fa` |\n| `terminalReason` | **exhaustive, closed enum** — set once, when the loop stops: `goal-reached` \u002F `step-budget-exhausted` \u002F `abandonment-threshold-hit` \u002F `auth-wall-blocked` \u002F `tool-error` \u002F `safety-refusal` |\n| `authState` | `n\u002Fa-no-auth-encountered` \u002F `authenticated-via-bootstrap` \u002F `auth-wall-untested` (the last aligns with `authWallUntested` below) |\n| `lifecycle` | `{status, unchecked}` from the stateful mission checklist above — `verified` \u002F `partial` (+ the unchecked step ids) \u002F `not-applicable` |\n\n**`severity` is load-bearing, not a label** — it must be the literal string\n`\"P0\"`, `\"P1\"`, `\"P2\"` or `\"P3\"`, and it is the field the auto-correlator\n(Phase 6b) filters on to decide what reaches `persona_audit_correlations`.\nA finding that spells it anything else is silently uncorrelatable; the\ncorrelator reports `p0p1-shape-mismatch` when your declared counts and this\nfield disagree.\n\n### Special cases\n\n- **404 \u002F page-not-found** → 1 retry after 5s; if still 404, emit P0 \"Target URL unreachable\" and stop\n- **Login wall** → branches three ways, in this order:\n  1. **No bootstrap was ever configured for this app** → emit P3 \"App\n     requires login; test scope limited to public surface\", continue with\n     public pages only, and set `authWallUntested = true` for Phase 4 (and\n     the session work-record's `authState = 'auth-wall-untested'`) — this\n     run did **not** cover the app's primary authenticated surfaces, and\n     the report must say so even when it finds 0 P0s there.\n  2. **A bootstrap was configured, and this looks like the connect-time\n     race** (short-lived token, session has been running a while, a\n     `storageState` file exists on disk) → this is a rig problem, not\n     signal about the product. Attempt the escape hatch in\n     `references\u002Fauth-bootstrap.md` (re-seed, then inject the refreshed\n     `storageState` via `browser_evaluate` and reload). If it clears the\n     login wall, continue the run normally — do **not** emit a finding for\n     the recovery; the report's Auth coverage line may note the\n     mid-session refresh as context, but it is not a P0-P3 item.\n  3. **The escape hatch fails after a fresh re-seed** (or the app's\n     session is HttpOnly-cookie-based and unrecoverable in-session) → this\n     is a genuine setup regression. Emit **P1** \"Auth bootstrap did not\n     authenticate the session\" (misconfigured\u002Fexpired `storageState` —\n     `references\u002Fauth-bootstrap.md`).\n- **Page-load timeout** → retry once with viewport reset; if it still times out, emit P1 \"Slow initial load (>15s)\" and continue\n- **Visible JS errors \u002F console errors** → emit P1 or higher with the exact error text\n\n---\n\n## Phase 3b — Consistency Mode (deterministic, code-driven)\n\n> Triggered by `--mode consistency`. **This is a completely different\n> execution model from the exploratory loop above.** When `--mode consistency`\n> is set, the LLM does NOT drive the browser — you delegate to the\n> deterministic runner which owns Playwright directly. Skip Phases 1-3 and\n> Phases 4-6 above; consistency mode has its own flow below.\n\nAuthoritative spec: [docs\u002Freference\u002Fconsistency-contract.md](https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fdocs\u002Freference\u002Fconsistency-contract.md).\n\n### Step C1 — Validate inputs\n\nRequired: `--mode consistency` + `--canary \u003Cname>` + URL. If any missing,\nprint usage and STOP.\n\n### Step C2 — Delegate to the runner\n\n```bash\nnode scripts\u002Fpersona-consistency-run.mjs --canary CANARY_NAME --url \"https:\u002F\u002Fyour-app.example.com\"\n```\n\nOptional: `--out .persona-test\u002Fsessions\u002F$SID.json` to write the session artifact to a specific path.\n\nThe runner resolves the manifest + canary, drives `canary.journeySteps[]` with\nno LLM in the loop, diffs DOM vs network ground truth, and checks\n`expectedContradictions`. → `references\u002Fconsistency-runner-and-report.md` §What the runner does.\n\n### Step C3 — Read the verdict from the exit code (do NOT parse stdout)\n\n| Exit | Verdict | Action |\n|---|---|---|\n| `0` | healthy — canary expectations met | Report success; surface contradictions count + any pending candidates |\n| `2` | rig-broken — canary expected ≥N contradictions, got fewer | **Stop the pipeline.** Surface the failureReason from the ledger; investigate manifest drift or attribute regression before running again |\n| `3` | fatal-rig — manifest missing \u002F canary schema invalid \u002F Playwright disconnected | Surface failureReason; the rig itself needs fixing |\n| `4` | ledger-persist-failed — couldn't write session JSON | Surface stderr; disk full \u002F permission issue, distinct from rig findings |\n| `5` | playwright-missing | Suggest `npx playwright install chromium`; the runner emits this hint to stderr too |\n| `6` | app-error — a journey action threw (e.g. TimeoutError on click) | This is an APP regression, NOT a rig issue. Surface the failing step + selector from the ledger |\n\n### Step C4 — Report\n\nRender the `CONSISTENCY MODE REPORT` fence (Canary, URL, Verdict, Exit,\ncounts; FINDINGS sorted P0 first, tie-break by surfaceId; OVERALL). On a\n`broken` verdict, lead with the `⚠ RIG BROKEN` callout.\n→ `references\u002Fconsistency-runner-and-report.md` §Report fence — read before rendering it.\n\n### Step C5 — Skip the persona debrief\n\nConsistency mode does NOT produce a Phase 5b debrief. The exit code + the\nfindings fence is the entire report — rig output, not persona perception.\n\nFull grammar + manifest schema + canary schema + flow details:\n[references\u002Fconsistency-mode.md](references\u002Fconsistency-mode.md).\n\n---\n\n## Phase 4 — Severity Model\n\n| Code | Label | Rule |\n|---|---|---|\n| **P0** | BROKEN | Primary flow fails; user cannot complete their intent |\n| **P1** | DEGRADED | Flow completes but is confusing, slow, or missing clear feedback |\n| **P2** | COSMETIC | Visual \u002F layout \u002F polish issue; flow works |\n| **P3** | OBSERVATION | Not a bug — preference, suggestion, or informational note |\n\nConfidence threshold: ≥0.6 to report, ≥0.7 for P0, ≥0.8 when calling a\nrecurring P0 from history.\n\n**`authWallUntested` rule**: 0 P0s on such a run means \"0 P0s on the public\nshell\", not on the app — OVERALL reads `Needs work` at best, never `Ready for\nusers` (it is one conjunct of Phase 5's predicate).\n→ `references\u002Frun-loop-detail.md` §Phase 4 — read when the verdict is contested.\n\n---\n\n## Phase 5 — Structured Report\n\nEmit the report inside this fence. Sort findings P0 first, ties by\nconfidence descending:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n  PERSONA TEST REPORT\n  Persona: \u003Cpersona>\n  URL: \u003Curl>\n  Focus: \u003Cfocus or \"exploratory\">\n  Device: \u003Cpreset_name> \u003CWxH> (touch=\u003Cbool>, resolved-from=\u003Cdescription|explicit|fallback>)\n  Driver: \u003Cbrowser_driver> (\u003Cbrowser_status>) — \u003CN> steps — \u003Cduration>\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nFINDINGS\n────────────────────────────────────────────────────\n  [P\u003Cn>] \u003Ctitle> (confidence: \u003Cn>)\n     Element:  \u003Cselector>\n     Observed: \u003Cwhat happened>\n     Fix:      \u003Cspecific recommendation>\n  ...\n\nCOVERAGE (relative to this persona's own session — not an inventory scan)\n────────────────────────────────────────────────────\n  Reached:        \u002F  →  \u002Fcellar  →  \u002Fcellar\u002Fadd   (3 unique screens, 9\u002F12 steps)\n  Loop ended:     \u003CterminalReason>\n  Focus:          \u003CdeclaredFocus>\n  Origin:         \u003CoriginPolicyResult>\n  Auth:           \u003CauthState>\n  Lifecycle:      \u003Cverified | partial (unchecked: \u003Cstep ids>) | not-applicable>\n\n  Not a surface-complete scan: this persona explored as it naturally would,\n  not exhaustively. For surface-complete coverage, run \u002Fnav-audit or\n  \u002Fclick-test against the same URL.\n\nOVERALL: \u003CReady for users | Needs work | Blocked>\n  Reason: \u003Cone sentence>\n```\n\nEvery field in the COVERAGE block is rendered directly from the session\nwork-record (Phase 3) — never recomposed here. `Reached` is `unique(pathname)`\nover `visitedSurfaces`; the step fraction is `stepCount.completed\u002Fcap`.\n\n**`Ready for users` requires a composed eligibility predicate, not a single\ncheck** — a clean run (no P0\u002FP1 findings) AND **every one** of:\n\n```\nterminalReason      === 'goal-reached'\nauthState           !== 'auth-wall-untested'\noriginPolicyResult  !== 'cross-origin-attempted-and-blocked'\nlifecycle.status    in ('verified', 'not-applicable')\n```\n\n**A reached goal with an unverified lifecycle is not a clean pass.** When\n`lifecycle.status` is `partial` (and nothing else fails), OVERALL is\n`Needs work` and the Reason line opens `Goal reached — lifecycle unverified`,\nthen lists the unchecked steps. The cap is enforced in code too:\n`record-persona-session` stores `Needs work` for a claimed `Ready for users`\nwhose payload fails any conjunct — including a MISSING `lifecycle`, which is\nunreported, not verified — and says so in the response's `verdictCap`.\n\nAny failing conjunct → OVERALL `Needs work`, and the Reason line names\n**every** failing conjunct. → `references\u002Frun-loop-detail.md` §Phase 5 — why\n`terminalReason` alone is not enough, plus a worked Reason line.\n\nIf `audit_link = true` and Phase 0d candidates match the persona findings,\nappend an **AUDIT CORRELATIONS** section mapping each P0\u002FP1 to a possible\nroot-cause audit finding. Keyword-matched — tag as \"verify before assuming\ncausation\".\n\nFull example output (report + debrief): `examples\u002Freport-and-debrief.md`.\n\n---\n\n## Phase 5b — Persona Debrief\n\nAfter the structured report, emit a **Persona Debrief** — 400–700 words\nin first person as the persona. Product discovery artefact, not a bug list.\nGrounded in session observations, not generic user opinions.\n\nFull tone rules, structure, and wrapper: `references\u002Fpersona-debrief-format.md`.\n\n---\n\n## Phase 6 — Save Session to Memory\n\nSkip if `memory_enabled = false`. Output `[Session not saved — memory disabled]` and stop.\n\nOmit `sessionId` — the CLI mints a collision-resistant one and returns it as\n`sessionKey`; pass `sessionId` only to re-post an existing session.\n\nRecord the session + trigger secondary persona stats update in one call:\n\n```bash\nnode scripts\u002Fcross-skill.mjs record-persona-session --json '{\n  \"persona\": \"\u003Cpersona>\",\n  \"url\": \"\u003Curl>\",\n  \"focus\": \"\u003Cfocus or null>\",\n  \"browserTool\": \"\u003Cbrowser_driver> (\u003Cok|degraded|blocked>)\",\n  \"stepsTaken\": \u003CN>,\n  \"verdict\": \"\u003Cverdict>\",\n  \"lifecycle\": {\"status\": \"\u003Cverified|partial|not-applicable>\", \"unchecked\": [\u003Cstep ids>]},\n  \"terminalReason\": \"\u003CterminalReason>\",\n  \"authState\": \"\u003CauthState>\",\n  \"originPolicyResult\": \"\u003CoriginPolicyResult>\",\n  \"p0Count\": \u003Cn>, \"p1Count\": \u003Cn>, \"p2Count\": \u003Cn>, \"p3Count\": \u003Cn>,\n  \"avgConfidence\": \u003C0-1>,\n  \"findings\": \u003CJSON array>,\n  \"reportMd\": \"\u003Creport text>\",\n  \"debriefMd\": \"\u003Cdebrief text>\",\n  \"commitSha\": \"\u003Cauto-detected if omitted>\",\n  \"deploymentId\": \"\u003Coptional>\",\n  \"repoName\": \"\u003Crepo_name or null>\",\n  \"personaId\": \"\u003Cpersona_id or null>\",\n  \"clickPath\": \u003CJSON array — the path you walked; see \"Building clickPath\" below>\n}'\n```\n\n### Building `clickPath` (reachability evidence for `\u002Fnav-audit`)\n\nOne entry per Act step that *moved or activated something* (click \u002F navigate \u002F\nopen \u002F submit):\n\n```json\n{ \"step\": \u003C1-based step number>,\n  \"action\": \"click|navigate|type|fill|select|hover|scroll|press|submit|wait\",\n  \"url\": \"\u003Cthe page URL AFTER the step settled>\",\n  \"targetText\": \"\u003Cvisible label of the control you acted on, ≤80 chars, or null>\" }\n```\n\n`targetText` is the control's visible label — typed input values stay out of\nthe payload. Omit the field (not `[]`) if you tracked no path.\n→ `references\u002Fsession-save-detail.md` §clickPath edges.\n\n**Response** — keep `sessionId` (the row uuid; later phases pass it as\n`personaSessionId`), not `sessionKey`. `statsUpdated: false` means the\npersona's `last_tested_at`\u002F`last_verdict` were not updated — report\n`statsReason` (`no-persona-id` \u002F `stats-write-failed` \u002F `persona-not-found`);\nnothing back-fills them. → `references\u002Fsession-save-detail.md` §Response fields.\n\nIf `verdictCap` is present, the store did NOT record the verdict you claimed:\n`{\"claimed\": \"Ready for users\", \"stored\": \"Needs work\", \"blockers\": [...],\n\"label\": ...}`. Correct the report's OVERALL line to match — the report and\nthe stored session must never disagree.\n\n---\n\n## Phase 6b — Verify the Correlation Summary (automatic — do NOT re-emit)\n\n`record-persona-session` (Phase 6) runs the correlator itself and writes the\n`persona_audit_correlations` rows. **Do not call `record-correlation` per\nfinding** — it exists only for manual repair (below).\n→ `references\u002Fsession-save-detail.md` §Phase 6b.\n\nYour only job this phase: **read `correlationSummary` from the Phase 6\nresponse and report it** (echo the counts — `candidates`, `route`, `fuzzy`,\n`missed`, `skippedExisting`, `malformed`, `writeFailed` — into the report).\n→ `references\u002Fsession-save-detail.md` §correlationSummary — read when a field's meaning is unclear.\n\n`attempted: false` means the correlator didn't run at all — `reason` says why\n(`disabled-by-flag`, `no-repo-identity`, `no-p0p1-findings`,\n`p0p1-shape-mismatch`, `session-write-failed`). **`p0p1-shape-mismatch` is\nnever benign**: you declared `p0Count`\u002F`p1Count` above zero but not one entry\nin `findings` carried a `severity` of `P0`\u002F`P1`, so nothing could be\ncorrelated. Fix the payload and re-post — every finding needs `severity`\n(see Phase 3), not a differently-named field. `attempted: true` with a `reason` still set\n(`no-candidate-runs`, `candidate-read-failed`, `existence-check-failed`) means\nit tried but couldn't compare against anything real — in BOTH cases, **zero\ncorrelation rows were written**, which is correct (an empty candidate set is\nnot evidence of an audit miss — never treat it as one). `malformed > 0`\n(findings missing `element`\u002F`observed`) were quarantined, correctly.\n`writeFailed > 0` is the one state\nworth flagging loudly in the debrief (a real DB write failed for some\nfindings; already logged to stderr by the correlator).\n\n### Manual repair — only for genuine corrections\n\nUse `record-correlation` **only** to:\n1. **Repair a false auto-emitted `audit_missed`** into a real match (supplying\n   a real `auditFindingId` retires the stale row automatically).\n2. **Emit `audit_false_positive` or `severity_overstated`** — human-judgment\n   types, never auto-emitted.\n\nCompute `personaFindingHash()` the same way the auto-correlator does\n(`references\u002Faudit-correlation.md`) so the repair matches the row it corrects.\n**When one of the two repair cases applies, run exactly one command per\nfinding being corrected** — payload via a Write-tool file under `.audit\u002F`\npiped on `--stdin`, not a shell-quoted `--json` string or heredoc\n(`references\u002Fshell-safe-payloads.md` §Manual correlation repair):\n\n1. Write `.audit\u002Fcorrelation-repair.json` with the Write tool:\n   ```json\n   {\n     \"personaSessionId\": \"the sessionId from Phase 6\",\n     \"personaFindingHash\": \"personaFindingHash() of the persona finding\",\n     \"personaSeverity\": \"P0|P1\",\n     \"auditFindingId\": \"the matching audit_findings.id, or omit if none\",\n     \"auditRunId\": \"the matched run id, or omit\",\n     \"correlationType\": \"confirmed_hit | audit_missed | severity_understated | audit_false_positive | severity_overstated\",\n     \"matchScore\": 0.0,\n     \"matchRationale\": \"one line\"\n   }\n   ```\n2. Pipe it in:\n   ```bash\n   cat .audit\u002Fcorrelation-repair.json | node scripts\u002Fcross-skill.mjs record-correlation --stdin\n   ```\n\nIdempotent (dedupes on session + hash + audit finding). Do **not** loop this\nover every P0\u002FP1 finding. → `references\u002Fsession-save-detail.md` §Manual repair,\nand `references\u002Faudit-correlation.md` for the classification rules.\n\n---\n\n## Phase 6c — Session History Readback\n\nAfter saving, surface patterns across prior sessions: recent runs, recurring\nissues (≥2 occurrences), persistent P0s (via the `persistent_p0s` view).\n\nSkip silently when Supabase vars are not set.\n\nFull query shapes + output format: `references\u002Fsession-history.md`.\n\n---\n\n## Phase 7 — Pair Mode (--pair)\n\nTriggered by `--pair \"\u003Cp1>\" \"\u003Cp2>\"` anywhere in `$ARGUMENTS`. Skip Phases\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=default -->\n_This site: `subcommand` — absence of `--pair` is a normal single-persona run — do not infer pair mode from two persona names being mentioned._\n\n0b–6c above and follow the flow below.\n\nTwo opposed personas surface disjoint findings. → `references\u002Fpair-mode.md` §Why.\n\n### Step P1 — Parse pair arguments\n\nParse from `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=subcommand; empty=ask-and-stop -->\n_This site: `subcommand` — both personas must be quoted after `--pair`; one missing prints usage and stops._\n\n1. **persona_a** — first quoted string after `--pair`\n2. **persona_b** — second quoted string\n3. **url** — URL in remaining args (or `PERSONA_TEST_APP_URL` env)\n4. **focus** — anything after the URL\n\nRequired: both personas + url. If missing, output usage and STOP.\n\n### Step P2 — Run persona A end-to-end\n\nRun **Phases 0c → 6c** for persona A as if it were a solo run (its own Phase 1a\ndevice resolution). Capture:\n- `report_a` (Phase 5 report text)\n- `findings_a` (the structured findings array used in Phase 5)\n- `verdict_a`\n- `session_id_a` (from Phase 6, may be null if memory disabled)\n\nThen close the browser session (`browser_close`) — persona B gets a fresh\ncontext, including a fresh cache-bust in Phase 1b. **Do not skip the\ncache-bust for persona B** — it's not redundant; the browser context was\ntorn down.\n\n### Step P3 — Run persona B end-to-end\n\nSame flow for persona B. Capture `report_b`, `findings_b`, `verdict_b`,\n`session_id_b`.\n\n### Step P4 — Diff the findings\n\nTwo findings overlap if **either**:\n- Same `element` selector AND same `severity`, OR\n- Jaccard similarity of `observed` text ≥ 0.6 (token-level after lowercasing\n  + stripping punctuation; ignore stopwords)\n\nClassify each finding into:\n- **CONSENSUS** — overlapping pair from A and B (high signal — both saw it)\n- **A-ONLY** — finding from A with no overlap in B (coverage signal — A's expertise caught it)\n- **B-ONLY** — finding from B with no overlap in A (coverage signal — B's expertise caught it)\n\n### Step P5 — Emit pair report\n\nPrint both solo reports in full, then the `PAIR DIFF` fence (both devices and\nverdicts in the header; CONSENSUS \u002F A-ONLY \u002F B-ONLY; overlap rate\n`consensus \u002F (consensus + a_only + b_only)`). → `references\u002Fpair-mode.md`\n§Step P5 — read before rendering it.\n\nOVERALL uses the solo vocabulary, and is the worse of the two solo verdicts\n(`Blocked` > `Needs work` > `Ready for users`): the pair is ready for users\nonly when both personas' runs independently are — including each one's\nlifecycle conjunct.\n\n### Step P6 — Skip the secondary debrief\n\nEach solo run already produced a Phase 5b debrief; do not write a pair debrief.\n\n### Step P7 — Session linkage\n\nWhen both solo runs were saved (memory enabled), record the pairing. The ids\nare each Phase 6 response's `sessionId` (the row uuid), **not** `sessionKey`:\n\n```bash\nnode scripts\u002Fcross-skill.mjs link-persona-pair --json '{\n  \"sessionA\": \"\u003CsessionId from persona A's Phase 6>\",\n  \"sessionB\": \"\u003CsessionId from persona B's Phase 6>\",\n  \"consensusCount\": \u003Cn>,\n  \"aOnlyCount\": \u003Cn>,\n  \"bOnlyCount\": \u003Cn>,\n  \"overlapRate\": \u003C0-1>\n}'\n```\n\nSend the counts you printed in Step P5 (`overlapRate` is optional and refused\nif it disagrees). A refusal exits 1 naming `PARENT_NOT_FOUND` or\n`PARENT_NOT_OWNED`. With the cloud store off (`\"cloud\": false, \"pairId\": null`)\nsay in the report that the pairing was not persisted.\n→ `references\u002Fpair-mode.md` §Step P7.\n\n---\n\n## Reminders\n\n- **You are the persona** — every click and judgement from their perspective\n- **Be specific** — \"button looks bad\" is useless; \"CTA [7] has no hover state on mobile 390px\" is actionable\n- **The verdict matters** — it drives whether the user ships or fixes\n\n---\n\n## Reference files\n\nThis skill's canonical flow is above. The files below cover specialised\nsituations — read them only when the trigger applies.\n\n| File | Summary | Read when |\n|---|---|---|\n| `references\u002Finput-acquisition.md` | Where a skill's arguments come from on any host, and what to do when there are none. | Reading $ARGUMENTS on a host that does not substitute it, or deciding what empty input means at a site. |\n| `references\u002Faudit-correlation.md` | Pre-test audit enrichment + post-test persona↔audit correlation emission — full rules. | `audit_link = true` AND (Phase 0d fetches audit candidates OR Phase 6b's manual-repair path is needed). |\n| `references\u002Fbrowser-tool-detection.md` | The browser-driver contract — capabilities, driver table, selection order, minimum sets, degraded\u002Fblocked evidence. | Phase 1 tool selection fails on first try, OR the user is on Windows and Playwright MCP tools aren't appearing. |\n| `references\u002Fauth-bootstrap.md` | Sanctioned auth-gated exploratory-testing pattern via MCP-server storageState, plus its connect-time-race escape hatch. | Phase 1 detects a login-gated target, OR Phase 3 hits a login wall (bootstrap configured or not). |\n| `references\u002Fverification-discipline.md` | Verification discipline — pinned citations, figure provenance, two-direction proof, attribution, consumer-side checks. | Phase 3 Record step \u002F Phase 5 — rendering the COVERAGE block, to apply §7's reciprocal warning (it renders only from the session work-record). |\n| `references\u002Fconsistency-mode.md` | Full consistency-mode grammar, manifest schema, canary schema, runner exit codes, contradiction kinds. | Phase 3b runs (i.e., `--mode consistency` was passed) and you need the full grammar reference; OR the user asks how the rig decides severity \u002F coercion \u002F negative-space. |\n| `references\u002Fconsistency-runner-and-report.md` | Consistency-mode runner steps (Step C2) and the Step C4 report fence, including the RIG BROKEN callout. | Phase 3b — about to run the consistency runner (Step C2) or render its report (Step C4). |\n| `references\u002Fpersona-debrief-format.md` | Full persona debrief generation rules, tone guide, and output wrapper. | About to write the Phase 5b debrief. |\n| `references\u002Fsession-history.md` | Post-session history readback — recurring-issue surface + cross-session pattern detection. | Phase 6c runs AND Supabase is configured. |\n| `references\u002Finterop.md` | How persona-test interacts with \u002Fship, \u002Fplan, and \u002Faudit-code\u002Faudit-plan — integration contracts. | User asks about cross-skill effects, OR a sibling skill needs to reference persona-test data. |\n| `references\u002Fshell-safe-payloads.md` | Why ADD and record-correlation payloads go through a Write-tool file on stdin, never a shell-quoted --json string. | Running `add` or a Phase 6b manual repair and tempted to inline the strings, OR asked why a file is used. |\n| `references\u002Fdevice-profile.md` | Phase 1a detail — device prep contract shape, mobile tag semantics, viewport-only limits, MCP launch flags, pair mode. | Phase 1a — the prep contract is unclear, a touch\u002FUA\u002FDPR bug is suspected, OR a pair run resolves two devices. |\n| `references\u002Frun-loop-detail.md` | Why the run-loop rules exist — cloud probe, auth at connect, cache-bust, stateful missions, Ready-for-users. | The cloud probe disagrees with env, the target is login-gated, OR a capped verdict is contested. |\n| `references\u002Fsession-save-detail.md` | Phase 6\u002F6b detail — session-id minting, clickPath edges, response fields, correlationSummary, manual repair. | Phase 6\u002F6b — a response field (statsReason, correlationSummary count) is unclear, OR a manual repair is needed. |\n| `references\u002Fpair-mode.md` | Pair mode detail — why opposed personas, the Step P5 PAIR DIFF fence, overlap interpretation, Step P7 responses. | `--pair` was passed — before rendering Step P5, OR Step P7 returned a refusal. |\n| `examples\u002Freport-and-debrief.md` | Sample full persona-test output — structured report + debrief fences and example content. | About to emit Phase 5 + 5b output and unsure of the exact fence format. |\n| `references\u002Fworktree-preflight.md` | Worktree preflight remedy — the skills:hydrate npm script to add when a linked worktree lacks the tooling tree. | A command dies with `MODULE_NOT_FOUND` in a linked worktree AND this repo has no `skills:hydrate` script. |\n\n— end of \u002Fpersona-test —\n",{"name":5,"description":7},"main","938efbef34b7c76f0d112f336d1ac4018224d186","skills\u002Fpersona-test","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills","https:\u002F\u002Fgithub.com\u002FLbstrydom","Studying M.Sc Computer Science at Heriot Watt","2026-11-13 04:00:27",{"source":8,"skillName":5,"commands":61,"prompt":71,"zipAvailable":72},[62,65,68],{"tool":63,"command":64},"npx","npx skills add Lbstrydom\u002Fclaude-engineering-skills --skill persona-test",{"tool":66,"command":67},"bunx","bunx skills add Lbstrydom\u002Fclaude-engineering-skills --skill persona-test",{"tool":69,"command":70},"pnpm","pnpm dlx skills add Lbstrydom\u002Fclaude-engineering-skills --skill persona-test","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002FSKILL.md",true,[74,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144],{"path":75,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":78,"sourceHtmlUrl":79},"examples\u002Freport-and-debrief.md","REFERENCE","参考文档",6661,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Fexamples\u002Freport-and-debrief.md",{"path":81,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":82,"sourceHtmlUrl":83},"gate-contract.json",27719,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Fgate-contract.json",{"path":85,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":86,"sourceHtmlUrl":87},"references\u002Faudit-correlation.md",8225,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Faudit-correlation.md",{"path":89,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":90,"sourceHtmlUrl":91},"references\u002Fauth-bootstrap.md",8931,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fauth-bootstrap.md",{"path":93,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":94,"sourceHtmlUrl":95},"references\u002Fbrowser-tool-detection.md",20710,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fbrowser-tool-detection.md",{"path":97,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":98,"sourceHtmlUrl":99},"references\u002Fconsistency-mode.md",10857,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fconsistency-mode.md",{"path":101,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":102,"sourceHtmlUrl":103},"references\u002Fconsistency-runner-and-report.md",2377,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fconsistency-runner-and-report.md",{"path":105,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":106,"sourceHtmlUrl":107},"references\u002Fdevice-profile.md",4206,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fdevice-profile.md",{"path":109,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":110,"sourceHtmlUrl":111},"references\u002Finput-acquisition.md",4511,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Finput-acquisition.md",{"path":113,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":114,"sourceHtmlUrl":115},"references\u002Finterop.md",3948,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Finterop.md",{"path":117,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":118,"sourceHtmlUrl":119},"references\u002Fpair-mode.md",3748,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fpair-mode.md",{"path":121,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":122,"sourceHtmlUrl":123},"references\u002Fpersona-debrief-format.md",3258,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fpersona-debrief-format.md",{"path":125,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":126,"sourceHtmlUrl":127},"references\u002Frun-loop-detail.md",5090,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Frun-loop-detail.md",{"path":129,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":130,"sourceHtmlUrl":131},"references\u002Fsession-history.md",2349,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fsession-history.md",{"path":133,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":134,"sourceHtmlUrl":135},"references\u002Fsession-save-detail.md",5446,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fsession-save-detail.md",{"path":137,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":138,"sourceHtmlUrl":139},"references\u002Fshell-safe-payloads.md",2094,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fshell-safe-payloads.md",{"path":141,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":142,"sourceHtmlUrl":143},"references\u002Fverification-discipline.md",28340,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fverification-discipline.md",{"path":145,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":146,"sourceHtmlUrl":147},"references\u002Fworktree-preflight.md",3413,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fpersona-test\u002Freferences\u002Fworktree-preflight.md",[149,167,178,196,208,239],{"skillNo":150,"name":151,"nameZh":6,"description":152,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":153,"categories":154,"keywords":160,"publishedAt":48},"01M6J45X4FDB2AD5BGS4R4QY8X","stated-absent","Negative fixture — the contract's stated string never appears in this file.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Fnegative\u002Fstated-absent\u002Fskills\u002Fstated-absent\u002FSKILL.md",[155],{"categoryNo":156,"categoryName":157,"taxonomyType":158,"taxonomyTypeDesc":159},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[161,162,163,164,165,166],"stated absent","STRING","contract","fixture","never","file",{"skillNo":168,"name":169,"nameZh":6,"description":170,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":171,"categories":172,"keywords":174,"publishedAt":48},"01M6J45TAX2PPHETZ5TJZG20QQ","nonexistent-implementation","Negative fixture — contract references an implementation file that does not exist.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Fnegative\u002Fnonexistent-implementation\u002Fskills\u002Fnonexistent-implementation\u002FSKILL.md",[173],{"categoryNo":156,"categoryName":157,"taxonomyType":158,"taxonomyTypeDesc":159},[175,176,177,163,164,166],"implementation","nonexistent implementation","references",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":182,"categories":183,"keywords":187,"publishedAt":48},"01M6J45P5GN52YFMZ81T7NN1VF","document-only-with-params","Negative fixture — a document-only gate illegally carries params (schema must refuse this fake-check pattern).","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Fnegative\u002Fdocument-only-with-params\u002Fskills\u002Fdocument-only-with-params\u002FSKILL.md",[184],{"categoryNo":185,"categoryName":186,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000002","文档处理",[188,189,190,191,192,193,194,195,164],"only","document","with","document only with params","check","schema","gate","pattern",{"skillNo":197,"name":198,"nameZh":6,"description":199,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":200,"categories":201,"keywords":203,"publishedAt":48},"01M6J45J1CCZ7AXDGQKBA9VC8M","lying-skill","Fixture skill for the gate-honesty self-check — every gate here is deliberately dishonest.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Flying-skill\u002Fskills\u002Flying-skill\u002FSKILL.md",[202],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[204,192,194,205,206,164,207],"lying skill","fixture skill","self","for",{"skillNo":209,"name":210,"nameZh":6,"description":211,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":212,"categories":213,"keywords":221,"publishedAt":48},"01M6J45CW0DNN7K2W39E3PQHEY","visual-audit","Math-first, deterministic visual\u002Fpaint inspection — the 4th UX lens, complementing\npersona-test (journey), click-test (page), and nav-audit (system). Drives Playwright +\ncomputed styles + bounding boxes + CDP forced-pseudo-state to audit declared-token\nconformance, light\u002Fdark theme parity, layout physics, and affordance signifiers. A VLM\nis advisory-only and never gates. CI gate is drift-only on the changed contracted surface.\nTriggers on: \"visual audit\", \"check styling consistency\", \"theme parity\", \"dark mode parity\",\n\"design token drift\", \"paint audit\", \"button\u002Fcard consistency\", \"focus ring audit\",\n\"\u002Fvisual-audit\".","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fvisual-audit\u002FSKILL.md",[214,215,218],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":216,"categoryName":217,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":219,"categoryName":220,"taxonomyType":158,"taxonomyTypeDesc":159},"01JBSEEDCAT000000000000032","设计师",[222,223,224,32,225,226,227,228,229,230,29,231,165,188,232,233,234,235,236,237,238],"visual audit","visual","audit","design token","click","inspection","lens","design","ci","math","cdp","journey","drift","focus","physics","layout","card",{"skillNo":240,"name":241,"nameZh":6,"description":242,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":243,"categories":244,"keywords":251,"publishedAt":48},"01M6J4569S40ZASFWW698CHBKP","ux-lock","Generate Playwright e2e specs. Two modes:\n  1. LOCK mode — pin a fix's DOM contract so it doesn't regress (default).\n  2. VERIFY mode — check that a \u002Fplan plan was actually implemented\n     by parsing its Acceptance Criteria section and driving the live URL.\nVERIFY mode grades a plan against a RUNNING app. To judge the plan document\nitself, before anything is built, that is \u002Faudit-plan.\nTriggers on: \"ux lock\", \"lock ux\", \"lock in the fix\", \"write regression spec\",\n\"generate e2e test for\", \"regression test for commit\", \"lock this fix\",\n\"verify the plan\", \"verify plan implementation\", \"check the plan was built\",\n\"audit frontend implementation\", \"did we ship the plan\".\nFull command syntax: see the Usage section in this skill.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fux-lock\u002FSKILL.md",[245,246,249,250],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":247,"categoryName":248,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":219,"categoryName":220,"taxonomyType":158,"taxonomyTypeDesc":159},{"categoryNo":156,"categoryName":157,"taxonomyType":158,"taxonomyTypeDesc":159},[252,253,254,32,255,256,257,224,41,258,259,30,260,261,262,263,38,264,34,265,266],"ux lock","lock","ux","commit","generate","acceptance","regression","syntax","write","fix","e2e test","default","judge","verify plan","frontend"]