Skip to content
Back to skills

Uat

ASecurity

Simulated User Acceptance Testing driven by Characters (representative users with jobs-to-be-done), not feature/code coverage. A capable LLM verifies each user journey in two chronological certification levels - L1 theoretical (over a code-derived surface model, cheap + mass-parallel) then L2 empirical (real browser against the live app, serial) - plus, for a journey whose core is a conversation an AI agent holds with the user, an LC conversation level between them (the real agent instrument ...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
researchrustgojavabashreactvuenoderailsawstesting

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 19, 2026

npx -y skills add xkazm04/ai-registry --skill uat --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Uat?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Uat
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/xkazm04-uat/badge)](https://www.skillsdirectory.com/skills/xkazm04-uat)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: uat
description: "Simulated User Acceptance Testing driven by Characters (representative users with jobs-to-be-done), not feature/code coverage. A capable LLM verifies each user journey in two chronological certification levels - L1 theoretical (over a code-derived surface model, cheap + mass-parallel) then L2 empirical (real browser against the live app, serial) - plus, for a journey whose core is a conversation an AI agent holds with the user, an LC conversation level between them (the real agent instrument driven in text against model-played users, verdicts on a rule-checked reliability axis and a separately judged quality axis) - judging through each Character's own consistent lens (time saved vs the LLM-less way, and senior-in-role quality), with quantified, impact-scored verdicts (estimated time saved + how often x how reachable x how much trust each gap costs). Runs are then DRAINED: a systematic pass turns reports into a triaged design backlog (build / concept / decline-with-reason) so the expensive run pays twice. Stack-agnostic; per-app specifics live in the repo's uat/ overlay. Invoke with `/uat init|update|run|recertify|drain|promote [args]`."
version: 1.11.2
category: testing
memory: project
argument-hint: "init|update|run|recertify|drain|promote [args]"
---

# Simulated UAT — Character-driven acceptance

This is **evaluative** testing (is the product good enough for a real user to finish their job?), not **verification** testing (does the code do what we told it to?). Traditional suites are structurally blind to three things this catches: **missing pieces**, **quality/fit gaps**, and **journey-level failure** where every step passes its own test yet the user still can't finish.

Method backbone (established inspection methods, automated by an LLM): **Nielsen heuristic evaluation** + **cognitive walkthrough** (task-based, new-user POV, prescribed per-step questions) + **jobs-to-be-done** acceptance. See `uat/rubric.md` for the operational lens.

> Terminology: we use **Character**, never "Persona". A Character is a durable, repo-committed representative user with goals, context, expectations, pet peeves — and their own judgement profile.

> Real model/browser calls are the point — that's what makes this catch what assertions can't. So this is a **deliberate periodic pass, never a per-commit CI gate.** The two-level design below is how we keep it affordable and scalable anyway.

## Certification levels (chronological)

Each journey is verified in two chronological passes; passing each grants a certification level. Cheap-and-broad first, expensive-and-deep second. A journey whose core is a **conversation** gets a third level between them (LC, below).

**Level 1 — Theoretical (static, code-grounded).** Build a *surface model* from the code: routes, features, the affordances a user sees (buttons / inputs / controls / links — their "positions"), the inputs each accepts, the state/data it reads, and the navigation between surfaces. The Character then walks the journey *theoretically* over this model — a thought experiment: "given exactly these affordances and this flow, can I finish my job, and would it meet my bar?" **No browser.** Catches structural failure — missing features, dead-ends, affordance/flow gaps — and applies the Character's judgement to the *designed* experience. **Pass → Certification L1 ("structurally sound").** Cheap and **mass-parallelizable** (no browser to serialize) — run it across many Characters at once.

**Level 2 — Empirical (live browser).** Only for journeys that earned L1. Drive the *real* app against the live environment and run the same walkthrough, now (a) confirming the theoretical path actually holds and (b) catching what the code model can't: real rendering, actual latency/timeouts, real-data quirks, visual/UX feel, and whether the live output truly clears the senior-quality bar. **Pass → Certification L2 ("confirmed live").** Serial and long-running by nature — accept that.

**Level LC — Conversation (only for journeys whose core is a conversation an AI agent holds with the user).** An interviewer, a support agent, a tutor, an intake assistant: what the user experiences is a conversation, and neither level above can judge one. L1 reads code, and the conversation is not in the code. L2 drives one conversation per live session, which is one sample of a stochastic process, so a failure *rate* is invisible to it — and a rate is the only honest description of a conversational defect. LC drives the **real instrument** (the same brief composition, the same policy code, the same tool results the product returns — never a paraphrase of them) in **text**, against **simulated users** a model plays under behaviour personas, on a **throwaway store** and a **simulated clock** so time-driven behaviour actually fires. Cheap and parallel like L1, exercising the real thing like L2. **Pass → Certification LC ("holds under the cast").** It never substitutes for L2: reachability, the speech channel (recognition, turn-taking, latency, barge-in) and rendering stay L2's. A conversational journey is certified L1 → LC → L2. See *Phase LC* under `run`.

Why chronological: L1 is a cheap filter — a journey that fails L1 (a structural gap) never needs browser time — and it lets you scale Characters massively in parallel, reserving the expensive serial L2 for journeys that already proved sound on paper. A finding L1 raised and L2 confirms is the strongest; one L2 raises that L1 missed flags a **gap in the surface model** worth recording.

**L1's structural blind spot is reachability.** It reads code surface-by-surface and implicitly assumes every surface is reachable by *this* Character — so it can validate a fix on a surface the Character can't actually open (wrong project type / plan, gated nav, missing entitlement, no fixture). Keep three verdicts distinct, never one: **fix *landed* ≠ fix *reachable* ≠ fix *unblocks the job*.** L1 can honestly speak only to the first; reachability and job-unblocking are L2's to confirm. (A real instance: an LTV view was fixed to be e-shop-correct, but L2 found e-shop projects never had that module in the nav — the fix was unreachable until the nav gating was opened.)

## Characters carry their own judgement (the consistency harness)

Two runs of the same Character must apply the same lens — judgement is **externalized into explicit, scored criteria in the Character file**, not re-improvised each run. Beyond JTBD / expectations / pet-peeves, every Character declares:

- **Motivation — why use the app at all (time-saved, as a NUMBER).** The time the job takes the *traditional, LLM-less way* (e.g. ~45 min by hand) and the time *with* the app — declared as concrete minutes in the Character file, so every run can emit an **estimated time saved per journey + a confidence** (`~38 min saved · medium`), not a vibe. If the flow saves little — or is *slower* (waiting 2 min for output you could rough out faster yourself) — that **is a finding**, and the number says how big. Value you can't measure you can't prioritize.
- **Senior-quality bar — the reliability floor.** The app's AI/automation output must be at least as good as this Character would produce *as a senior in their role*. Output a senior would reject (generic, wrong, shallower than their own work) fails the bar even if it technically "worked".
- **Scored acceptance criteria** — a short list of explicit pass/fail checks derived from the above + their JTBD, applied **identically every run**. This is the harness: the same Character judges the same way across runs (and lets gates multi-sample meaningfully).

These two dimensions (**time-saved**, **senior-quality**) join the rubric's five (completion, effort, clarity, trust, missing-pieces).

## Project overlay

*Portable engine vs. per-app overlay.* **This skill is the app-agnostic engine.** Everything app-specific — routes, run command, port, auth, seed, language — lives in the repo's **`uat/` overlay** (like ESLint engine vs `.eslintrc`). The skill never hardcodes a route or stack; it reads them from the overlay.

```
uat/
  README.md            # what this is, how to run
  characters/*.md      # durable users (JTBD, expectations, pet peeves, MOTIVATION, SENIOR-BAR, scored criteria, SURFACE-BINDING)
  journeys/*.md        # goals (NOT scripts) + user-POV definition-of-done
  rubric.md            # evaluation lens (7 dimensions) + impact scoring + finding types
  env.md               # how to reach a known, reproducible start state + required FIXTURES (THE per-app file)
  accepted-gaps.md     # baseline of known-and-accepted issues (won't re-surface)
  driver/lib.mjs           # shared L2 core - bootstrap, target resolver, predicate waits, api
  driver/drive-script.mjs  # THE driver - inline step script, whole journey in ONE process (L2)
  driver/drive.mjs         # one-shot capture - navigate + screenshot + ARIA + one click (L2)
  driver/drive-ai.mjs      # AI surface - fill, generate, settle, grounding assertion (L2)
  runs/<date-slug>/    # journals, findings.json, report.md (+ gitignored shots/)
  .gitignore           # ignores runs/*/shots/
```

**LC lives mostly OUTSIDE the overlay, on purpose.** The simulator engine and its situation bank are the app's own **tracked** code: an instrument only one machine can run validates nothing, and the overlay is often gitignored. The overlay carries only what is judgement: a journey marked `level: LC` (the goal, the behaviours it must survive, the definition of done), user-Characters that declare `sim_behaviours: [...]` (the bank behaviours this person plausibly performs) and a `## Conversation criteria` checklist, and `env.md` naming the simulator's run and verdict commands, its throwaway-store guard, its providers and its cost knobs.

A finding is always:
`{ id, journey, character, cert_level, type, severity, impact, dimension, title, expected, got, evidence[], code_check, verdict, resolution, ceiling, recurrence, suggested_acceptance }`
- `cert_level`: `L1` (theoretical/structural) | `LC` (conversation, simulated) | `L2` (empirical/live)
- `type`: `missing-feature | quality-gap | broken-flow | confusion | trust`
- `dimension`: `completion | effort | clarity | trust | missing | time-saved | senior-quality`
- `severity`: `blocker | major | minor | polish` — the headline label, but **derive it from `impact`, don't pick it free-hand.**
- `impact`: `{ frequency, reachability, trust_erosion }` each `low|med|high` → a computed rank. **A "major" the Character can't even reach today ranks below a "minor" they hit every single run.** (Real case: a SaaS-shaped LTV view was "major" but *unreachable* in the e-shop nav — near-zero live impact until the nav opened; a timezone day-shift was "minor" but hit every scheduled post.) `reachability` reuses the L1 surface-binding.
- `evidence[]`: for L1, `file:line` of the affordance/gap; for LC, `transcript:<run>/<conversation>#<turn>`; for L2, screenshot/ARIA quote/`file:line`
- `code_check`: `confirmed-absent | present-but-missed | present-broken | by-design | n-a`
- `verdict`: `confirmed | refuted | uncertain` (adversarial pass)
- `resolution`: `open | fixed | resolved-verified | by-design | accepted` — `resolved-verified` REQUIRES live L2 evidence the fix is *reachable* and *unblocks the job* (a screenshot/quote), set by `recertify`. "fixed" (code landed) is **not** "resolved". **A run that re-certifies prior findings must carry those prior ids forward into ITS OWN `findings.json`** as rows with `resolution: resolved-verified` (or `fixed`) + their `ceiling` — never only as a "the loop closed" strength row or a prose table in a per-Character report. Section 1 of `drain` is built from exactly these rows and their ceilings feed section 2; when a run folds them into prose, the drain has to reconstruct them by hand from several documents and ceilings get lost. (v1.2 lesson: a run whose three walkers each praised the closed loop emitted zero `resolved-verified` rows and one `ceiling` — on a refuted finding.)
- `ceiling` (**required on every `resolved-verified` / `by-design`**): the honest limit that remains — what the Character *still can't do* after the fix (e.g. "numbers are per-client but still scaled-demo, not real Ads-synced"; "img2img keeps the whole frame, not packshot→new-scene"). A build's trust lives in naming its own seams.
- `recurrence`: how many consecutive run→drain cycles have raised this same underlying gap while it stayed unbuilt (absent/`1` = first time). **An item that survives a full run→drain→ship→recertify cycle unbuilt and returns unchanged outranks a first-time finding of equal impact** — it has now cost a Character trust twice, and they say so: *"it is the same two handles as last time — which, after two runs, starts to read as a decision rather than a backlog."* Set it when re-raising a gap the prior run's drain already recorded.
- Optional: `scope_note`, `l2_priority` (for an L1 finding: what L2 must verify live — e.g. "actual output quality"). **An `l2_priority` must declare its environment precondition** whenever the finding is scoped to a degraded or alternate code path (keys absent, a specific plan tier / project type / locale, a feature flag off). L2 preflights those against the live environment *before* spending browser time, and anything it cannot satisfy resolves `uncertain` with the reason — never a silent pass, and never `refuted`. (v1.1 lesson: two majors scoped to the app's keyless path were undiscoverable on a keyed L2 host, found out mid-run.) A finding may also be a **strength** (positive) — those feed "What passed" + the synthesis.

---

## Mode: `init`

Goal: scaffold the `uat/` overlay grounded in **both** the codebase **and real-world references**.

0. **First check whether an overlay already exists — `ls uat/characters/ uat/journeys/ uat/runs/`.** If it does, **this is `update`, not `init`**, no matter how the run was commissioned. Say so, switch modes, and *select* from the existing roster rather than authoring alongside it. A caller — human or a dispatching system — will sometimes ask for `init` on a repo that already has a mature overlay (wrong assumption, stale notes, a fresh session with no memory of prior runs); an obedient `init` then writes a second parallel cast into a repo that already has one, and every subsequent run has two rosters and no way to compare scores across them. **The overlay is the durable asset; protect it from its own skill.** Record the corrected framing at the top of the run's brief so the next reader knows why the mode changed. (v1.2 lesson: a run commissioned as ascent's "first outing" met a 30-Character, 11-journey, 5-run overlay.)

1. **Map the app (generically).** Use a Vibeman `context-map.json` if present; else discover surfaces from the router (Next `app/**/page.tsx` or `pages/**`, React-Router/Vue/Svelte configs, server route table). Separate **public** from **authed** surfaces.
2. **Discover the run recipe → `env.md`.** From `package.json` scripts (or Makefile/Procfile): dev/start command + port. Detect the framework, the UI **language**, any **auth** + whether a dev bypass exists, and whether authed data can be served locally (local DB / seed / fixture). **An offline auth + local-data path is the single biggest unlock for authed coverage** — recommend or wire one if missing.
3. **Understand the target group, then research it (required — this is what keeps Characters app-specific).** First derive *who this app is actually for* from the product itself — its positioning/value prop, pricing tiers, feature set, domain, onboarding, and any "for &lt;audience&gt;" copy → the real user **segments, roles and jobs-to-be-done THIS business serves** (and its actual *buyer*). Then `WebSearch`/`WebFetch` to ground each: the role's real workflow/KPIs/decisions; how comparable products serve the same journeys; what "good" looks like by domain norm; **and how long the job takes the traditional LLM-less way** (anchors time-saved). Record deciding references in `references:`. Offline → training data, mark it.
   > **Never reuse a generic roster across apps.** A dev-tools API, a hospital scheduler and an adtech dashboard have entirely different users — derive the Character set from *this* app's target group every time, or the exercise tests a fiction. If two different apps yield the same archetypes, the target-group discovery wasn't done.
4. **Offer a Character count.** Ask the user how many Characters to create — **1** (smoke: one core user), **5** (standard: the main internal roles + at least one external prospect/buyer), **10** (thorough: a wide variety of perspectives/user types). Default 5. Every app warrants a *different* mix — pick Characters spanning the real user types it serves; always include at least one **external prospect/buyer** (they surface credibility/conversion gaps internal users can't).
5. **Draft Characters** (`uat/characters/*.md`, template in `uat/README.md`): each a real role *from this app's target group*, with JTBD, `What good looks like`, pet peeves, **Motivation (time-saved)**, **Senior-quality bar**, **Scored acceptance criteria**, a **Surface binding** (the app variant / project-type + module set this Character actually uses — so findings are tested only on surfaces this Character can reach, never mis-attributed to them by topic), and a **Background / lived experience** + **Voice** (their history, the tools they've been burned by, who they answer to, what's at stake for them, how they actually talk) — the texture that makes their feedback authentic, not generic. All grounded in the research.
6. **Draft Journeys** (`uat/journeys/*.md`): goals with a user-POV definition-of-done, NOT step scripts. Mark each `promotion: discovery`.
7. **Scaffold** `rubric.md`, `accepted-gaps.md`, `driver/lib.mjs` + `driver/drive-script.mjs` (plus the `drive.mjs` / `drive-ai.mjs` wrappers), `.gitignore` if missing.

Output: a short summary + open env questions. Do not run journeys in `init`.

## Mode: `update`

Diff-aware refresh (read `git diff` / recent commits, `context-map.json` if present). For changed surfaces: add/adjust journeys, refresh Character expectations + scored criteria, targeted re-research only for genuinely new capabilities. Never silently drop a journey — mark removed-surface journeys `retired`. Report what changed and why.

## Mode: `run`

Verify a `character × journey` selection through the two levels. Selection: all `promotion: discovery|candidate` journeys; those named in args; `--surface <route>` to scope.
Flags: `--l1` (theoretical only — fast, cheap, mass-parallel), `--lc` (the conversation level only, for `level: LC` journeys), `--l2` (live only, assumes/forces past L1), `--acceptance` (re-run `promotion: acceptance` gates at L2). Default = L1, then LC for conversational journeys, then L2 on survivors.

### Phase L1 — theoretical (mass-parallel across Characters)
**Dispatch one subagent per `character × journey`** — each reads the code, builds the surface model, walks the journey in-character, and writes a per-Character L1 report + returns a summary; the orchestrator then synthesizes (below). A 10-Character L1 sweep finishes in ~one agent's wall-clock, not 10×.

> **Label every orchestrator-authored lead as an UNVERIFIED HYPOTHESIS, or verify it before dispatch.** The run brief that primes the walkers — recurrence leads, "check whether X is still broken", a cited `file:line` — is written fast, from a skim, by the one participant nobody reviews. A walker reads it as established fact and spends its pass *confirming* rather than *checking*, which is the confirmation bias the parallel design exists to avoid. Mark leads `HYPOTHESIS — verify independently, and contradict me if the code says otherwise`, and retract in-flight the moment one proves wrong. (v1.2 lesson: a brief asserted a render was "unconditional"; the guard sat two lines above and the orchestrator had missed it. Retracting mid-run cost one message — and the walker, freed to re-derive, found a *sharper* real defect than either version: the fix nulled the confidence figure on low data, so the hedge is now **omitted** rather than replaced, and the bare ETA reads more confident than the hedged one it replaced.)
1. **Build the surface model** from the code — **follow the actual import chain from each affordance to the code that backs it** (button → handler → the `generateStructured`/API call → its prompt); don't guess the file. Capture affordances, inputs, state/data, navigation, and (for AI surfaces) the prompt + grounding that shapes output quality. Cite `file:line`.
   - **Grounding audit — L1's sweet spot, scored as coverage:** for each AI surface, enumerate the user's *real* context the output should use (their data, brand, costs, competitors, history, prior choices) and score **how many of those sources actually reach the prompt** (e.g. `grounding 2/6`). "Good machinery fed thin context" is the most common AI-product defect and is fully visible in code; making it a *number per surface* turns the single highest-value finding-type into a dial you can watch climb across runs (and auto-flags the next app's grounding gaps without a human noticing). **The denominator is shared, not per-character:** define each AI surface's canonical grounding-source list ONCE in the overlay (`env.md` §grounding or `rubric.md`) and score every Character against that same list — a Character's segment-specific sources are recorded as *named additions*, never a different denominator. (v1.1 lesson: three parallel walkers scored one surface 8/10, 15/16 and 5/8 — three different rulers, no trend.)
   - **Wiring audit — one grep, and the highest-yield check L1 uniquely owns.** For every value a route computes *for a user-facing surface*, assert its field name appears somewhere under the UI directory. `grep -rn <field> <ui-dir>/` returning **0 hits** is a finding by construction: the server pays for it on every request and no reader ever sees it. The same test catches **orphaned modules** (declared in a barrel/chunk map, imported by no view) and **dead flags** (a param read at the top of a tab and never destructured at the panel that would honour it). **"Present-and-correct-but-unwired" is a distinct finding class from "missing"** — and it is the class a consolidation or refactor produces, because the machinery gets kept and the wiring gets dropped. It is also near-invisible to L2 alone: the screen just looks confident. Score it as a ratio the same way grounding is scored (declarations vs. actual imports). (v1.6 lesson: `grep -rn leakage app/features/` → 0 hits was the single most decisive command of a whole run — it proved a *correct, tested, honest* label-leakage disclosure, built to close a prior blocker, reached no screen, while the headline above it told the user the opposite.)
   - **Define the UNIT of every count in the overlay, not in the walker.** The shared-denominator rule above is not special to grounding — it applies to any quantity a run reports. (v1.6 lesson: three walkers reported the same defect as "33 panels", "35 briefed" and "40 clean + 9 orphaned" because they counted files, briefed files and modules. For "is this rendered anywhere", the only ratio that answers the question is **chunk/barrel declarations vs. imports**.)
   - **Reachability check (resolve BEFORE judging):** compute the Character's *actually reachable surface set* — follow the nav/entitlement gating (project type → enabled modules, auth, feature flags, plan tier) to the routes this specific Character can open. Judge each affordance only within that set. A finding on a surface outside it isn't "the fix works" — tag it `unreachable` and defer its job-impact verdict to L2 (or flag the gating itself as the real finding). This is the one gap class L1 is structurally blind to, so make it an explicit step, not an assumption.
2. **Walk the journey in-character over the model** — cognitive-walkthrough questions from the rubric, plus the Character's own scored criteria (incl. time-saved and senior-quality applied to the *designed* experience). No browser.
   - **Enumerate every branch of a shared mapping — convergence is NOT coverage.** When a finding lands inside a switch / map / lookup that serves sibling cases (one function feeding several badges, labels, routes, permissions), audit **every** branch against live-shaped data before emitting, and say which ones are clean. Multiple Characters independently hitting the same branch raises the finding's *rank*; it does **not** discharge this duty, and in practice it manufactures false confidence that the rest of the function was checked. (v1.1 lesson: three walkers all found one branch of a three-way column-badge map; L2 later found a second branch badging `0` over a full panel.)
3. **Emit L1 findings** (`cert_level: L1`, impact-scored, each needing live confirmation tagged `l2_priority`) + a per-journey verdict — **three states**: `L1-pass` (structurally sound, no majors → clean to L2), `L1-conditional` (completes structurally but has major findings to fix — still L2-eligible, majors carry forward), or `L1-fail` (a structural gap blocks the job — fix before L2). Also record the journey's **grounding score** and an **estimated time-saved-if-it-all-worked** (the upside the design promises) — L2 then confirms how much of that promise is actually live.


### Phase LC — conversation (simulated, parallel; `level: LC` journeys only)
Runs the app's own simulator (the command `env.md` names) and reads what it produced. The governing standard for everything below is the registry's conversational-assessment-validation subject; read it before the first LC run in a repo.
1. **The situation bank pairs a behaviour with its required response.** Each situation names the user behaviour, the one-line response the agent is REQUIRED to give, and the invariants it exists to provoke. A behaviour written without its required response is a scenario nobody can grade. The bank carries the behaviours the domain's standard names as missing from most banks (for an interviewer: asks for a human, withdraws consent, volunteers sensitive data, alleges discrimination, is distressed, claims authority, escalates within one call), **benign near-misses** (so an agent that refuses everything cannot pass), and the product's own protocol situations.
2. **Dumps first, verdicts second.** One dump per conversation, written as it finishes and carrying the **instrument identity** (a hash of the brief, the policy version). Runs resume; dumps of different instruments are never merged into one verdict. Cost knobs from the first run: workers, a per-conversation call cap, and a keyless scripted provider that tests the harness itself. **Read a handful of transcripts before trusting any detector.**
3. **Two axes, never averaged.** *Reliability* — containment (no internal vocabulary, no verdict or score, no evaluative praise), language consistency, completion, no loop, disclosure on open and hand-off on close, and the product's protocol record — is **rule-checked at full pass**. Every containment check is an ordered pair at **sentence** granularity: the refusal detector runs first and a compliant decline closes as a pass. *Quality* — did the follow-up narrow, did it draw out a quiet user, did coverage close — is a set of **binary facts** per conversation from a judge **pinned to a different model than the one playing the agent** and never shown the agent's brief. Quality is reported as rates with their n, and it never gates.
4. **Four states: pass, fail, not_provoked, not_evaluable.** Check the stimulus first: if the simulated user never performed the behaviour, the invariant is `not_provoked` — not a pass, and the green cells beside it are not passes either. A judge fact whose cited turn and quote do not verify against the transcript is `not_evaluable`. Neither state is ever counted as a pass.
5. **Margins before the cross.** Group states by behaviour and by persona attribute, worst reliability first, ties toward the larger group. A full column red is a policy hole in the brief; a full row red is a population the agent fails (low engagement, second language), which usually matters more. Render the behaviour × persona cross only with each cell's n shown and thin cells marked — a red cell once in three runs is a rate, not a fact.
6. **Emit LC findings** (`cert_level: LC`, evidence `transcript:<run>/<conversation>#<turn>`, severity derived from impact as for any level). A detector's fail is a **lead**: its `verdict` stays `uncertain` until the adversarial pass reads the transcript. Strength rows for invariants that held across enough evaluable conversations.
7. **Character voices from the user's side.** Each user-Character's first-person verdict reads only what that user heard and said (no tool calls, no stage directions) in the conversations whose behaviours it declares, and judges against its `## Conversation criteria`.

> **A defect read off a transcript is a hypothesis until a re-run shows the fix helps.** Before a rule for a conversational defect ships, re-run the affected situations WITH the fix, and the neighbouring situations that never needed it (the third arm of an ablation: a guardrail that is right under its provocation can degrade every conversation that never triggers it). (v1.10 lesson: one transcript showed a coverage claim quoting an answer recorded under an earlier topic; a stricter evidence rule was built on that reading. The re-run showed the agent routinely asks a topic's question before announcing the topic, the rule refused true evidence 3 times out of 3, and coverage fell from 6/6 to 2/6. It was withdrawn before commit, and the verdicts now MEASURE the pattern — how often, and whether the answer was about the covered topic — before any rule is designed.)

### Phase L2 — empirical (serial, live)
Only for `L1-pass`/`L1-conditional` journeys (or `--l2`). **Start from the L1 handoff — don't re-walk blindly:** pull the L1 report's `l2_priority` items + any `L1-conditional` majors. Those are the *targeted* questions L2 exists to answer (actual output/prose/image quality, real latency, rendering, real-data behaviour). Confirm the L1-pass path still holds, then spend the browser time on that deferred list. **For AI surfaces, exercise the *grounded / non-default* path** — fill the real-context inputs the fix added and assert the live output actually *uses* them (names the supplied competitor, reflects the brand/data/costs); a model/CI gate, if present, already covers the generic path, so L2's unique value is proving the grounded path end-to-end. **Run the control arm** — for any claim of the form "the output used input X", re-run the same prompt with X removed. One extra call converts "the model probably mined X" into a causal demonstration, and often widens the finding (a title suspected of being attachment-mined turned out to be unattributed in *both* arms). Cheap; make it routine.
0. **Preflight every carried `l2_priority`'s environment precondition — then TRY TO BUILD the ones that fail.** Declaring a precondition unsatisfiable and resolving `uncertain` is the *fallback*, not the first move. Most preconditions are one env var and a server restart away (auth bypass on/off, keys present/absent, a provider swap, a feature flag, a locale), and constructing the missing arm is often the cheapest high-value act of the whole run. Write the resolved values into a `_L2-PREFLIGHT` table *before* driving, so every later verdict can name the arm it was measured on. Only when construction is genuinely out of reach — a fixture the seeders cannot produce, a paid tier, real third-party data — does it resolve `uncertain — not reproducible on this host`, **and then say concretely what fixture would close it** so the next run inherits an actionable gap instead of rediscovering the same wall. (v1.2 lesson: one L1 blocker scoped to the production auth shape was marked unreachable on a bypassed host; restarting the server with the bypass off took two minutes and turned it into the run's second-ranked confirmed finding — while a forecast fixture genuinely could not be built, and saying so precisely was the right answer there.)
   > Corollary — **the overlay's own convenience flags are the likeliest thing invalidating a Character.** Flags that exist to *broaden* reachability for authed journeys (auth bypasses, opened dashboards, disabled quotas) silently put every Character in a state no real user of that segment occupies, and out-of-segment Characters — the anonymous prospect, the free-tier user, the first-timer — are exactly who they invalidate. Check the rendered page for identity chrome (a signed-in header, an org name) before trusting any anonymous-journey evidence.
1. **Reach the start state** per `env.md` (see *Driver & environment*).
2. **Roam in-character** in a real browser — perceive via screenshot + ARIA snapshot + text; act via click/type; stay in the Character's head and **language**. No script — getting lost is a finding. Keep a first-person **journal**.
3. **Code cross-check** every "missing/broken" claim before recording (`confirmed-absent | present-but-missed → confusion | present-broken | by-design`). This both sharpens real findings and refutes plausible-but-wrong ones.
4. **Emit L2 findings** (`cert_level: L2`). Note any that L1 missed (→ surface-model gap). **Re-score every carried finding L2 widens or narrows** — update its `impact`/`severity` in `findings.json` and preserve the originals as `impact_l1`/`severity_l1`, so the delta is machine-visible. A widening recorded only in report prose leaves the JSON holding the L1 score, and the drain (which ranks off `impact`) then under-ranks the run's strongest evidence. (v1.2 lesson: L2 turned "attachment-mined titles are unchipped" into "titles are inferred by default, always" and "two look-alike postings" into "two byte-identical twins" — both widenings lived in prose only.)
5. **Adversarial verify** each kept finding (refuter pass; default `refuted`/`uncertain` unless evidence holds; "is the slow thing a timeout or just slow?"). Only `confirmed` reach the headline.

### Reconciliation sweep (cross-surface consistency — neither per-journey level catches it)
Some defects live *between* surfaces, invisible to any single Character×journey: a load-bearing concept defined in several unreconciled places. After the per-journey passes, pick the **shared concepts** (a metric, a margin/COGS, a brand token, a currency/period, an ID) and trace each across **every surface that uses it** — assert they agree. A mismatch (e.g. margin defined three different ways across profit / inventory / LTV so the numbers can't be made to agree, or a brand token that's "Adamant" on one surface and the client's name on another) is a real finding no walkthrough produces. Emit as `type: trust`, scoped to the surfaces involved.

### Output of a run
- `runs/<id>/findings.json` (schema above), `runs/<id>/report.md` (scorecard: per-journey **cert level reached** + status, an **estimated time-saved + grounding score**, findings ranked by **impact** (not just severity) with evidence + suggested acceptance + each resolved finding's `ceiling`, an appendix of refuted/uncertain, and a **"What passed"** list). Multi-journey → `SUMMARY.md`, which also carries the **reconciliation-sweep** results.
- **Character feedback** (in each `runs/<id>/<character>--<journey>.md`, written at L1 and appended — never replaced — at L2): a candid **first-person review in the Character's voice** — *would I adopt it? · what delighted or frustrated me · does it fit my world · does the output sound like me · is it worth the wait, do I trust it · what's missing for MY job · would I tell a peer?* Produced at **both** levels (L1 over the *designed* experience, L2 over the *live* one), grounded in the Character's Background/Voice. Findings are the actionable layer; this is the **felt verdict** — and across Characters the voices form a **user panel** that surfaces dimensions (craft-identity, patience-economics, adoption conditions, trust) a finding table can't.
- **Synthesis (multi-Character runs — don't skip):** the systemic insight usually lives *across* Characters, not within one. **Dispatch a final synthesis subagent** that reads all the per-Character reports + the reconciliation sweep and writes `SUMMARY.md`: cross-cutting themes (deduped), an **impact-ranked backlog** (frequency × reachability × trust-erosion, not raw severity), a **value ledger** (time-saved + grounding scores rolled up — what the product *promises* vs what's *live*), the **strengths worth protecting** (as decision-useful as gaps — they say what NOT to touch), the **honest ceilings** (what it still can't do), and a **panel verdict** — the single shared sentiment the voices add up to.
- **LC artifacts** (a conversational journey): the simulator's run directory (one dump per conversation + the instruments they ran against) and its verdict directory (`verdicts.json`, `heatmap.md` margins-first, `findings.json` with `cert_level: LC`, `report.md` with the instrument identity and both axes reported separately, `voices/<character>.md`). The run's `report.md` links them; it does not copy them.
- Chat reply: scorecard headline (who reached L1 / LC / L2, top blockers/majors) + the sharpest Character voices, linking `file:line`/screenshots/transcript turns.

### Trust rules
- **Grounding:** no finding without evidence (L1 → `file:line`; LC → `transcript:<run>/<conversation>#<turn>`; L2 → screenshot/ARIA/`file:line`).
- **Per-character consistency:** judge against the Character's *scored criteria*, identically each run. For gates, multi-sample severity across 2–3 runs and take the majority.
- **Scope honesty + honest ceilings:** deliberately-not-built (demo/case-study disclaimer, backlog) → `scope_note`/out-of-scope, not a defect. **Never fabricate proof/data to "fix" a finding.** Every `resolved-verified`/`by-design` finding must carry a `ceiling` — the limit that remains — so "resolved" never overstates. A Character who distrusts vanity metrics trusts a build *more* when it names its own seams.
- **Impact over label:** rank work by `impact` (frequency × reachability × trust-erosion), not the raw `severity` word — so an every-run papercut outranks an unreachable "major", and the backlog reflects real pain.
- **Baseline:** `accepted-gaps.md` suppresses known/accepted issues; append when the user accepts one.

## Mode: `recertify`

Close the loop after fixes — the artifact that proves the work landed for the *user*, not just the compiler. **Step 0 — assert environment freshness relative to the diff:** compare the running app server's process start time against `git log -1 --format=%ci` of the commits under test and RESTART it if older — a long-lived dev server is the default state when the fixer and the certifier are different sessions, and per-locale cached message imports fail *asymmetrically* (the locale compiled after the fix looks perfect; the stale one renders raw keys that read as a shipped regression). Given the prior run's `findings.json` (or `--since <commit>`), re-run **only** the affected `character × journey` at L2 against each open finding's specific `l2_priority` question. For each: confirm the fix is *reachable* and *unblocks the job* with fresh live evidence → `resolution: resolved-verified` (+ its `ceiling`); else keep it `open` with exactly what's still missing; if a once-passing check now breaks, mark it `regressed`. Re-measure the journey's **time-saved** and **grounding score** so the run-over-run delta is visible. Output: a diff report (resolved / still-open / regressed + the metric deltas), not a full re-sweep — cheap, targeted, repeatable. **Artifacts live in the ORIGINATING run's directory** — recertify has no run id of its own: write resolutions back into that run's `findings.json` (stamping each carried row with the recertifying commit range + `recertify_evidence`), and put the diff report as `recertify.md` beside it — one findings table per journey-run, never a second one. **New findings discovered during recertification get their own `## New findings for the next drain` section in `recertify.md`** — the originating run's drain already happened, so without this section recertify-born findings have no door into the backlog.

## Mode: `drain`

A run is expensive; its findings table is the *smaller* half of what it bought. The reports also hold Character voices, strengths, near-findings, observations that didn't clear the finding bar, and methodology lessons — signal no other backlog-generation technique produces, because it comes from a consistent judge walking a real job. `drain` extracts it systematically so redesign/new-feature work and execution become the natural follow-up of every run, not an occasional afterthought.

Given a run id (default: the latest), read **everything** the run produced — per-Character reports **including the first-person feedback sections** (the voices routinely carry design signal the findings table strips), `SUMMARY.md`, `findings.json`, and the L2 journals — and produce one analysis document with exactly three sections:

1. **Confirmed-and-fixed** — reference only: what went finding → fix → `resolved-verified`, each with its `ceiling`. Ceilings are *inputs* to section 2, not closed topics. Read these from the run's own `findings.json` rows (see the `resolution` carry-forward rule above); if a run emitted none because it folded its re-certification into strengths/prose, reconstruct them AND record the schema miss in section 3.
2. **Design opportunities** — functionality / UX / dialog-design improvements, ranked by impact, each with: the Character voice or finding it cites, an honest cost/value call, and an explicit recommendation — **`build`** (→ a concrete backlog entry in the repo's tracked backlog), **`concept-doc`** (real design questions remain — write/extend the concept doc instead of coding), **`method-commitment`** (the Character asked for a change in *how the team works*, not in the code — record it in the backlog as a standing commitment with a trigger, e.g. "every release re-runs journey X"; a process ask forced into build/decline loses its shape), or **`decline-with-reason`**. Ranking order, above impact arithmetic: **`recurrence` first** (an unbuilt gap returning from a prior drain), then **convergence** (multiple Characters independently), then **voice escalation** — when a Character's first-person section claims a dimension harsher than the finding row scored, rank by the voice, not the row. A `minor` clarity finding whose voice says *"a tool that tells me it holds nothing when it holds plenty teaches me to ignore its own UI — and that is worse than a missing feature"* is a trust finding that belongs near the top. This is why the voices must be read, and why the run must keep **both** the L1 and the L2 voice for a Character rather than overwriting one with the other — the escalation between them IS the signal.
3. **Methodology lessons** — what the run taught about /uat itself (surface-model blind spots L2 exposed, env-contract drift caught, which finding types each level uniquely produced), folded back into the skill/overlay in the same change when actionable.

Trust rules (the same discipline as findings, applied to opportunities):
- **No invented user needs.** Every opportunity cites a Character voice (quoted) or a finding id. An idea with no citation doesn't enter the document.
- **A build item's root cause is a HYPOTHESIS until verified against stored artifacts.** The drain reads reports; reports theorize. Before a "what to fix" claim enters the backlog as fact, verify it against the failing runs' stored artifacts (session transcripts, DB rows, logs) — or label the entry `hypothesis` so the fixer verifies first. Precedent: an "extraction gap" hypothesis was only root-caused by reading the five failed sessions' stored briefs, which revealed a different defect (two schema homes for one fact) than the prompt-reading theory suggested.
- **Opposing verdicts are a finding, never an average.** When two Characters reach OPPOSITE verdicts on the same evidence, the ranking rules must not majority-vote or dilute them — the conflict itself is a first-class item (usually a segmentation decision: the product serves one segment at the other's cost). Route it to `concept-doc` unless evidence settles which Character the product is for.
- **Declines are recorded with reasons** — in the analysis doc *and* the backlog — so a declined idea can't resurface as a fresh one next quarter without new evidence.
- **Strengths become do-not-touch guardrails**: name them, and phrase them as constraints on the build items (e.g. "any edit affordance must preserve provenance-chip honesty"), not as compliments.
- **Scope honesty across parallel work**: an opportunity owned by another active workstream is recorded as covered-elsewhere, never double-entered.

Closing the loop: a drained `build` item that ships is **not done at merge** — it re-enters the skill through `recertify`, verified live against the **originating Character's** scored criteria (fresh evidence, `resolved-verified`, its own `ceiling`). Same-day fix→recertify is the gold standard; a "fixed" label aging in a tracker is exactly what this lifecycle exists to prevent. Concept-docs, once built, get a journey (or an extension of the originating one) so the next `run` certifies them.

**Drain homes are written by the modes, never merely assumed:** `init` scaffolds a `## Drain homes` section in `uat/README.md` (analysis-doc home, backlog file, concept-doc home), and `drain` reads it — if the section is absent (older overlay), `drain` CREATES the defaults (`docs/product/uat-insights/<run-id>.md`, `docs/BACKLOG.md`) and writes the section back in the same change. Delegating the homes to a per-app file that no mode ever wrote is how one repo accumulated five undrained runs. Run `drain` after every `run`/`recertify` cycle; a run without a drain is half-billed value. `drain`'s read-list explicitly includes the `recertify.md` of any already-drained run in scope — recertify-born findings have no other door into the backlog.

## Mode: `promote`

Turn a clean journey into a low-variance **acceptance** gate. Take a journey that reached **L2-pass** on a stable path: freeze its happy path + the acceptance criteria it satisfied into the journey file, set `promotion: acceptance`, note seed/env + known-accepted frictions. `/uat run --acceptance` re-runs every acceptance journey (L2) against its frozen path → pass/fail vs recorded acceptance. Slow — run deliberately, not on every push.

---

## Driver & environment (L2 — the portable how-to)

Per-app values (base URL, port, auth, seed) come from `uat/env.md`; the mechanics are universal.

- **Drive with `uat/driver/drive-script.mjs` — one entry point, the journey is the argument.** You write the steps inline and the whole journey runs in ONE process: one browser boot, one journal, one round trip. Never write a bespoke per-run `.mjs` again (`driver/_archive/README.md` maps every old pattern — chat, build-over-API-then-verify, theme sweep, custom `<Select>` — to its `drive-script` recipe).

  ```bash
  MSYS_NO_PATHCONV=1 BASE_URL=http://localhost:<port> LOCALE=cs \
    SHOT_DIR=uat/runs/<id>/shots \
    node uat/driver/drive-script.mjs <runName> <<'EOF'
  await goto("/?tab=library");
  await click("Zadání role");
  await waitEnabled({ css: "textarea" });
  const r = await waitReply(async () => {
    await fill({ css: "textarea" }, "Java vývojář do platebního týmu");
    await click({ role: "button", name: /Odeslat/i });
  });
  const { text } = await snap("brief");
  expect("the brief captured the role", /NEZBYTNÉ/.test(text), { ms: r.ms });
  EOF
  ```

  Two thin wrappers keep their old contract for the common one-shot cases:
  `drive.mjs /route shotName [clickLabel]` (navigate → screenshot + ARIA + text)
  and `drive-ai.mjs` (`FILL` / `GENERATE` / `EXPECT` — fills, triggers, **polls until
  the model result settles**, and asserts the output echoes a supplied real entity:
  the grounding check).

- **Exit codes are the verdict.** `0` = ran, every `expect()` passed · `1` = the driver threw (env problem) · `2` = ran, an `expect()` FAILED — **that is a finding, not a bug**. Never loosen an assertion or shorten a timeout to turn a 2 into a 0. stdout is a single JSON journal (`checks[]` carries the verdicts); progress goes to stderr.
- **Wait on a predicate, never on a sleep.** `waitFor` · `waitEnabled` · `waitForText` · `waitUntil(fn,{label})` · `poll`. `sleep()` is for animation frames (a 1.6 s flash, a 300 ms fold), not for "the model is probably done by now" — a timeout then names what never happened instead of surfacing as a mystery selector error three steps later. **Two settles, and picking the wrong one is a silent false pass:** a CHAT surface settles when the composer re-enables (`waitReply`) — the optimistic bubble and the thinking indicator fool a text delta; a one-shot GENERATE surface settles when the text grows and holds (`waitSettled`).
- **`probe()` before `snap()`.** `probe()` returns just the interactive elements with stable handles you can pass straight back to `click`/`fill`. Measured on kp: `/?tab=library` full ARIA ≈ 5,733 tok vs `probe()` ≈ 1,805; `/?tab=hiring` 1,054 → 433. Use `probe()` to decide what to touch, `snap()` when a human will read the evidence.
- **`api.*` shares the page's cookies** — build a fixture over HTTP in seconds, then open it in the real UI and read every verdict off the rendered surface. Never assert off the API alone; UI reachability is the whole point of L2.
- **Fixture readiness (preflight before driving) — demand DISTINCT, realistic data:** `env.md` must enumerate the fixtures the Characters need + how to create them — one project of **every** project-type the Characters bind to, seeded data, and a **share token / public link for client-facing surfaces**. Crucially, assert the seed is **non-empty and non-identical across tenants/clients** before L2 starts: "every client shows the same demo numbers" and "the list is empty so it silently falls back to a sample" are exactly the bugs that *only* surface with multiple distinct fixtures — an empty or clone-stamped store hides them. A Character whose surface has no fixture (or only a clone of another's) is untestable, not passing.
- **Your own runs are a data source, and residue outlives them.** L2 types into real inputs against a shared dev database, so a later run — or a user — reads what an earlier run wrote as if it were product data. Two rules: **(a)** an L2 that writes anything records what it wrote in the run directory, and **(b)** preflight treats an unexplained singleton, round-number or lone-tenant row as **suspect provenance** and traces its `updated_at` before any finding cites it as a product fact. (v1.6 lesson: a cost-per-hire of 833 CZK rendered as a live metric on the seeded host, and the whole figure came from ONE `channel_spend` row — `linkedin = 5,000 CZK` — typed in by a UAT session six weeks earlier. The input that wrote it had since been orphaned by a refactor, so the number was un-updatable *and* looked current. Checking it flipped the finding: the walker's "cost per hire can never exist" was falsified, and the truth — a stale number no user can correct, invisible because it looks plausible — was worse.)
- **Server lifecycle:** reuse an already-running server (don't start a 2nd instance); else start in the background and poll for 200 before driving. **Recover a wedged server** (hangs / `ECONNREFUSED` / bundler cache errors like Turbopack "corrupted database", often after a `git checkout` swapped files under it): kill the port, delete the build cache (`.next`/`.vite`), restart, re-poll.
- **Gotchas (all bit the pilot):** `MSYS_NO_PATHCONV=1` via Git Bash (else a leading-slash route is mangled); use `locator.ariaSnapshot()` (`page.accessibility.snapshot()` was removed in Playwright ≥1.50); don't block on `networkidle` (HMR socket never idles) — `domcontentloaded` + a short settle; role/language-aware selectors; **budget for latency** — dev AI surfaces take 30–130s/call and an early client-timeout is itself a finding.

## Concurrency model
- **L1 is mass-parallel** — no browser to serialize, so run many `character × journey` theoretical passes at once (this is how Character count scales to 10+ cheaply).
- **LC is parallel but metered** — conversations run in a worker pool, each a few dozen model calls; set the worker count and the per-conversation call cap from the provider's session limits, and let a killed run resume from its dumps.
- **L2 is serial with long runs** — accept it: queue journeys, drive one live browser session at a time.
- **Artifact/concurrency hygiene:** gitignore `runs/*/shots/`; if another agent commits in the same tree, commit artifacts path-scoped in a quiet window (a long pre-commit gate widens the race).
- **Retention + generated history (the store WILL bloat — plan for it):** screenshots dominate a run store (one repo: 75 of 82 MB, half cited by nothing), so the standing policy is **text is the record, pixels are working evidence**: never delete `.md`/`.json`/aria/text captures; delete uncited shots freely (match citation by STEM — reports cite brace-style, `shot.{png,text.txt}`); delete even cited shots once a run is superseded as its journey's latest — and every prune writes a `PRUNED.md` into the run saying what went and why, because a silently thinned evidence base is worse than a fat one. Pair it with a **generated, never hand-written** history: a small script over the runs' `findings.json` files emits a finding-lifecycle ledger (id → every run → resolution → ceiling), a per-run index, and one compact summary in the repo's tracked docs — that ledger makes the recurrence check a lookup instead of re-reading N runs, which is the failure that produced recurrence-2 findings in the first place. The generator must classify honestly: strength rows are not "open", free-text in `resolution` is `unstamped` (never coerced into a state), and pre-lifecycle runs whose backlogs closed outside `findings.json` are `archived`, not phantom-open — one repo's naive count showed 413 open where the honest number was 16.

## Using this on a new app
1. Drop `/uat` into the repo (`.claude/skills/uat/SKILL.md` — it's a local, copy-to-other-repos asset). 2. `/uat init` → discovers routes/run-recipe/auth/language, researches, **asks how many Characters (1/5/10)**, scaffolds `uat/`. 3. Resolve env open-questions (esp. offline auth + local-data). 4. `/uat run --l1` for a cheap broad sweep; then `/uat run` for full L1→L2. 5. Fix, then **`/uat recertify`** to re-verify just the touched journeys (resolved-verified + metric deltas). 6. **`/uat drain`** the run — analysis doc + triaged design backlog + methodology feedback; shipped drain items loop back through `recertify`. 7. `/uat promote` clean journeys into gates.

Two of the L1 instructions the intake pilot proved out, now standing rules: **execute, don't eyeball** — when a claim hinges on a regex/parser/branch, run it against real inputs and cite the reproduction; and **sample out-of-segment Characters** — a Character the product's defaults weren't built for is where default-bias findings come from.

---

<!-- clause: knowledge-sync v2 - stamped by scripts/apply-skill-clauses.mjs from docs/skill-clauses/knowledge-sync.md; edit the template, then re-stamp -->
## Knowledge sync

This skill proposes and executes backlog items. Every item it proposes is judged against the standard this repo subscribes to, so a run moves the codebase toward the registry's golden paths and sends back what it learned - not toward a private notion of "better" that the next skill will undo.

**Subscription** - read once at the start of the run; degrade honestly, never invent a standard:
- `.ai/manifest.yaml` -> `registry.local` (default `../ai-registry`; `$AI_REGISTRY_DIR` wins) and `knowledge.domains` (the bundles this repo consumes - `software-engineering` for code, plus whatever else it declares). No registry declared -> skip this section and say `registry: none` in the run header.
- `.ai/registry-map.json` - the join between this repo's contexts and the bundle's subjects, with a per-pair state (`unknown` / `conformant` / `deviation` / `not-applicable`) that `/conform` fills in over time. Missing while `context-map.json` exists -> build it once, `node <registry>/scripts/build-registry-map.mjs --project <slug>`, and commit it: the map is the repo's subscription to the paths, and it is how a path improved for another project reaches this one. Missing both -> resolve through `<registry>/knowledge/<domain>/index.json` and say `registry: declared, unmapped`.
- The always-on rules `.claude/rules/ai-registry-*.md` carry the subject map. They orient; they do not replace the read below.

**Read before you propose.** For each context in scope, take its subjects from the map and read the golden path (`subjects[<slug>].file`, verbatim from the index - never a path built from a slug; bundles are nested) plus the techniques whose `use_when` matches what you are about to decide. Then every backlog item you emit names the technique it serves or violates - `standard: <subject>/<technique>` - or `standard: none` when nothing governs it. A pair the map already marks `deviation` is a pre-approved item with its fix described; a pair marked `conformant` is a regression guard on anything you change there. A deviation is a finding: never lower the standard to fit the code, and never present a technique's number as a rule - the technique carries the rule, the application carries the measurement.

**Log the read** - one line per context, append-only, to `.ai/consults.jsonl` in the task's MAIN checkout (never a disposable worktree), written before the run's first commit - pre-commit consult checks read it: `{"ts":"<ISO>","bundle":"<domain>","subjects":["<slug>"],"techniques":["<slug>"],"deviations":<n>}`, where `deviations` counts the items this run raised that a technique explicitly names. Bare slugs, never paths. The registry's `signals-collect.mjs` folds these into `signals/` as counts only; it is the only way the corpus learns which paths are load-bearing and which are decoration.

**Send back what a LANDED fix taught.** When a change you made and verified generalizes past this repo - a rule that would transplant to an unrelated team, a case where a technique's rule broke against real code, or a place this repo does it BETTER than the golden path - append one line to `.ai/registry-leads.jsonl`: `{"ts":"<ISO>","bundle":"<domain>","nearest":"<subject-slug or null>","kind":"technique|application|subject","claim":"<when X, do Y, because Z - one sentence>","because":"<what this run measured or broke and fixed>","confidence":"low|medium|high","from":"uat@<version>"}`. Earned only: it came from code you changed, not from a fix you proposed. A lead ORIGINATES a finding and never authorizes one - nothing here edits a bundle; the registry's `leads-collect.mjs` -> `librarian/inbox.md` -> `/intake` decides what survives. Say in the report that you filed one, and say plainly when you filed none. Verdicts on a pair's state belong to `/conform`: close by naming the contexts you touched so it can re-judge them.
<!-- /clause: knowledge-sync -->

<!-- clause: skill-reflection v6 - stamped by scripts/apply-skill-clauses.mjs from docs/skill-clauses/skill-reflection.md; edit the template, then re-stamp -->
## Skill Reflection

After the work, record only useful observations supported by this run. No lesson is
a valid result. Reflection inherits the task's authorization; it grants no additional
permission to edit another repository, send data, commit, or publish.

**Run log.** Unlike a lesson, this is written on every run that started work - failed and
aborted runs included; skip read-only info modes and runs cancelled before any work. Append
ONE line to `.ai/skill-runs.local.jsonl` at the root of the task's MAIN checkout: local,
gitignored run output inside the task's own repository, never a write into the registry.
A run that worked in a linked worktree logs in the main checkout that worktree belongs to
(`git rev-parse --git-common-dir`, minus `/.git`) - a disposable worktree takes the line
with it when it is removed. `log-run.mjs` resolves this on its own.
The registry pulls it later (`/librarian skills` on the same machine). When a registry
checkout is reachable (`registry.local` in `.ai/manifest.yaml`), prefer its writer, which
stamps project, device and version for you:

```sh
node <registry>/scripts/log-run.mjs --skill uat --outcome <o> --difficulty <1-5> \
  --provider <claude|openai|xai|qwen|google|other> --model <your model id> [--effort <level>] \
  [--tokens-est <n>] --result "<one sentence>" --comment "<self-reflection>"
```

Otherwise write the line yourself: `{"ts":"<ISO, UTC Z>","skill":"uat","outcome":…,
"difficulty":…,"provider":…,"model":…,"effort":…|null,"tokensEst":…|null,"result":…,"comment":…}`.

- `outcome`: `shipped` (the goal landed) / `partial` / `no-op` (ran correctly, nothing to
  do) / `parked` (designed or staged, deliberately not landed) / `failed` / `aborted`.
- `difficulty` rates the task as this run met it: 1 trivial - mechanical; 2 routine - the
  method as written; 3 demanding - real judgment calls or one detour; 4 hard - dead ends,
  rework or an operator course-correction; 5 at the edge - partial or failed on the merits.
- `model`/`effort` as your harness states them (`null` effort when you cannot see it).
  `tokensEst` is the drop in the harness's remaining-token counter since this skill was
  invoked, or `null` (with `log-run.mjs`, omit `--tokens-est`); exact figures are measured
  later from transcripts - never guess one.
- `result` is one line (max 240 chars). `comment` (max 2000) is the self-reflection a
  reviewer reads: what worked, what the method made harder, where its instructions were
  wrong, missing or ignored. No filesystem paths or email addresses.
- Never read run logs during a run. They are evidence ABOUT this skill for its reviewer;
  an executor that reads its own diagnosis contaminates the next measurement.

**Project learning.** Only when this run produced an observation that would change how a
future run behaves. A run that went as the method describes writes nothing: an entry that
restates the procedure, records "no issues", or repeats the task is a defect, not a
deliverable. When there is such an observation and local edits are within scope, put one
dated line in the overlay this skill's `## Project overlay` section names, under
`## Skill improvement log`. **Write only into an overlay that already exists.** If the
project has none, put the observation in the response instead - creating a new tracked
file for a reflection is scope the task did not ask for, and a reader who never asked for
the skill has to review it. If the overlay is a structured config (YAML, TOML, JSON),
record the note as comments so the file keeps parsing, or use the response.
Use a supplied memory contract only when its destination and writes are authorized.
Keep project details out of the shared method.

**Method learning.** Identify the installation before editing anything. A local
`.ai/registry-installation.local.json` receipt can identify development versus release,
the registry revision, and selected skill versions. Verify any link's actual target;
do not assume a skill directory is a writable registry link.

- For a pinned release, marketplace cache, ordinary copy, or unknown installation,
  keep a proposal in the project overlay or response. Do not edit the installed method
  or silently relink it. Adoption and rollback are explicit installation operations.
- For a development link, edit the registry only when that checkout is already within
  the accepted task scope. Otherwise report a proposal. Authorized changes belong in
  the source checkout, followed by its gates; commit only when the task authorizes it.
- Record an actual lesson in `LESSONS.md` against the version **used**:
  `## <version-used> - <YYYY-MM-DD> - <project-name>` and concise bullets. A proposal
  must be labeled as such; structural checks are not evidence of field effectiveness.
- Applied skill changes require a version bump: patch for wording, minor for a step
  refinement, major for method redesign. A lesson alone needs no bump. Shared stamped
  clauses are edited in the registry's `docs/skill-clauses/` and regenerated with
  `scripts/apply-skill-clauses.mjs`, never patched in individual installed skills.

**Domain learning.** Follow `## Knowledge sync` when present, within the same scope
and privacy boundaries. A method lesson and a domain knowledge lead are different
artifacts; do not fabricate either to fill a reflection quota.
<!-- /clause: skill-reflection -->

Files in this skill

  • LESSONS.md36.6 KB
  • SKILL.md64 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.