agentleFS
dork-labsdork-labs/dorkos/.agents/skills/auditing-ui/SKILL.md

auditing-ui

Skill11 starsChanged 50 days agoMITView on GitHub

Runs a lens-based UI/UX audit of the client and turns its findings into tracked, fenced work items, and measures the rendered page for a design review. Use when auditing the interface for quality gaps, running or scoping an audit (full, one lens, one surface, or a diff), driving the real-browser leg of an audit, measuring a page or a PR's change against the design review floors (phone sizes, icons, targets, contrast, focus), synthesizing raw lens findings into batches, or deciding how audit findings enter the tracker.

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 4 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read the charter before every run.

    line 17
  2. Very specific:

    Read audits/runs/ui/profile.md at the start of every run, pulse, and execute.

    line 33
  3. Distinctive:

    Degrade explicitly: When no app is runnable (no free port, no build, a broken dev server, or a profile that says there is none), say so in …

    line 163
  4. Very specific:

    Every /ui-audit:run and /ui-audit:pulse emission creates one per-run type/meta item, "promotion decision for audit run <date>", and every …

    line 260
  5. Distinctive:

    Add it to the charter's Standing directives with its date and the operator's own framing.

    line 315
  6. Very specific:

    If it is true of every domain and not only this one, it belongs in audits/README.md instead.

    line 318
  7. Distinctive:

    If it changes the lens set or a lens key, update the Lens classes section too: the rotation and the stamps are keyed off it.

    line 319
  8. Distinctive:

    Not yet, then Blockers, Should and Polish, each with the issue, the rule, the measured value against its floor, and the fix.

    line 349

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Auditing UI

An audit reads the interface through several independent lenses at once, then merges what they saw into one ranked report and a set of PR-sized batches. It writes zero code.

The rules live in the charter, not here: audits/ui.md (domain) inherits audits/README.md (cross-domain). What passes lives in contributing/design-review.md: the numbered rules a finding cites and the floors the browser leg measures. The charter is the authority on the lens set, the lens keys, and which lens is surface-local versus whole-tree. This file is the procedure, and it is the single home for the procedure: the commands under /ui-audit:* point here rather than restating any of it.

Read the charter before every run. If audits/ui.md is missing, run /ui-audit:init first.

1. Durable state

Three things outlive any single run and live at the domain root, audits/runs/ui/:

PathWhatWritten by
profile.mdhow work lands in this repo/ui-audit:init
stamps.jsonper-lens last-audited commit + rotation cursorrun and pulse
<date>/one run's report.md, raw/<key>.md, and its ledger when untrackedrun and pulse

<date> is ISO YYYY-MM-DD.

The repo profile is consulted, not decorative

Read audits/runs/ui/profile.md at the start of every run, pulse, and execute. It answers questions this procedure deliberately does not hard-code, and each answer changes behavior:

  • Landing style — a repo with no merge queue must not inherit arm-then-verify or a bare gh pr merge --auto; execute follows the profile's answer, not this repo's habits.
  • Changelog — whether a batch owes a fragment, and in what shape.
  • Isolation — worktree per batch, or something else.
  • Formatting gate — the exact command that satisfies CI, run last before every push.
  • Tracker mode — the adapter path in §5, or the markdown ledger.
  • Browser leg — the dev command and a free port, or the fact that there is none, which is what §3's degrade rule keys off.

A missing profile is not a reason to guess. Say it is missing, name /ui-audit:init, and fall back to the most conservative reading (no queue, no fragment, no browser leg) — but only for a run that never intended real tracker emission (dry or validation). A run that does intend real emission captures the profile then, per /ui-audit:init contract 3, rather than silently downgrading to the markdown ledger: the conservative fallback is for runs that were never going to emit, not a quiet substitute for one that was.

Stamps, and the first run

{
  "lenses": {
    "copy": { "lastAuditedCommit": "a1b2c3d", "lastRun": "2026-09-20" },
    "responsive": { "lastAuditedCommit": "9f8e7d6", "lastRun": "2026-09-13" }
  },
  "rotation": { "cursor": "dry" }
}

Stamps are per lens, never one global stamp: a lens skipped for six weeks still sees six weeks of change when it next runs. A whole-tree lens's lastAuditedCommit records recency only — when it last ran — and is never used as a diff base, since whole-tree lenses cannot be diff-scoped (§2). The rotation cursor names the next whole-tree lens due, in the charter's Lens classes order. Write a lens's stamp only when that lens actually ran, and only after its findings are recorded; a lens that did not run keeps its old entry untouched. Creating stamps.json for the first time sets rotation.cursor to the first whole-tree lens in that order (cva), since nothing has run yet and the first one is therefore the one due.

Bootstrap rule: a lens with no stamp has no diff base, and the audit refuses to invent one. diff scoping and /ui-audit:pulse require an existing stamps.json. With no stamps file, both stop and say so, naming the fix: run full (or a scoped run covering those lenses) to establish the baseline. A lens missing from the file stops a diff run the same way; the pulse instead skips that lens and names it in its report, with the same fix, and runs the lenses that do have a stamp. Neither ever widens a lens to the whole tree: that turns a cheap pulse into a ten-million-token run nobody consented to, which §2's cost gate exists to prevent. run therefore takes an explicit scope and has no default.

2. Scope the run

ModeWhat runsWhen
fullall twelve lenses over the whole clientrare, expensive, a programme kickoff
lens:<key>one lens, whole treevalidating a lens, or a rotation slot
surface:<route>surface-local lenses over one route and its component treeafter building or reworking a surface
diffsurface-local lenses over what changed since each lens's stampthe recurring pulse

Lens keys come from the charter's lens list, which is their only authority.

diff covers surface-local lenses only. Whole-tree lenses are properties of the whole tree; scoping them to a diff produces confident nonsense. They are reached through rotation instead: one whole-tree lens per pulse, cycling in the charter's order.

Coverage, which is the real budget. Tokens are what a run costs the operator; they are not a stop condition an auditor can meter. Give every auditor a coverage budget instead: read its lens's source of truth in full (the token and theme files for tokens, the shared primitive directory for cva and dx, the playground registry for playground), then run one scripted sweep per violation class and open the hits. Stop when the sweeps are exhausted, and state in the coverage note where you stopped and what you did not reach. Never hand an agent a clock.

Cost. Budget roughly 0.5M to 1M subagent tokens per lens, more for the ones that read the whole shared layer. The twelve-lens run that produced the September 2026 report came to roughly ten million. Print an estimate before spawning, and on full get explicit consent first.

A scoped run is not a small full run. On lens:<key> and surface:<route>, the whole-tree scope stands but the reporting rules relax: collision class beats batch size (§4), a single-lens run synthesizes inline (§4), and the report says plainly which lenses did not run, so nobody reads a one-lens report as a verdict on the interface.

3. Fan out the auditors

One auditor per lens. Each auditor reads the charter itself (not a paraphrase of it), gets its lens brief, and writes its raw findings file to audits/runs/ui/<date>/raw/<key>.md before returning a structured summary. Raw files are the evidence the synthesizer works from; a summary alone is not enough.

  • Parallel — spawn N agents, however your harness spawns agents, one per lens. This is the fast path and the one the report's turnaround assumes.
  • Sequential — run the lenses one at a time in the same session, appending each raw file as it completes. This is a first-class path, not a degraded one: a single machine adopting this should start here, and a sequential run produces exactly the same report.

Either way the auditors are read-only: they must not modify a source file, and must not work inside a checkout another agent is writing to.

The prompt template is assets/auditor-prompt.md. Fill its placeholders; do not rewrite its rules section, which restates the charter's binding constraints.

The browser leg

Code reading finds most of what a lens looks for. It does not find clipped layouts, strings that escape their container, console errors on load, or motion that reads wrong. Those need a running app, driven at desktop and phone widths.

Which lenses get one, and when. Five lenses have findings a browser can confirm or upgrade: responsive, states, motion, clutter, and tokens (for theme drift and anything the cascade decides at render time). The command fills the auditor prompt's {{#BROWSER_LEG}} block for exactly those, on every scope, whenever the profile supplies a dev command and a free port — and for no other lens, since the rest read structure a browser cannot adjudicate. The leg may be skipped even then when nothing in scope is browser-adjudicable, provided the report says so. No dev command in the profile means the degrade rule below applies to all five. No profile at all is different: when the scope includes one of these five lenses, capture the profile now (or ask) rather than silently skipping the browser leg — the same rule as §1's emission case, because a dropped browser leg here is exactly the kind of thing a missing profile must not quietly cause. This is the predicate; nothing else decides it.

Look, don't touch. These rules are binding, and this is the only place they are written:

  • Your own port, and only your own. Boot your own client on a port you chose and hold. On this machine :6242 is the operator's server and :6241 the orchestrator's client: both are someone else's, always. Stop only the process you started, by the PID you hold or by lsof -ti :<your port>. Never pkill or killall (AGENTS.md Hard Rule 7).
  • A standalone Playwright script, never a shared MCP browser. A shared browser is a single contended resource; parallel auditors driving it interleave and produce garbage. Write a small node script that navigates, resizes, screenshots to the session scratchpad, and exits, then read the screenshots. The installed package is @playwright/test, resolvable from apps/e2e (bare playwright is not installed and will not resolve).
  • No mutating clicks. The API behind your client may be the operator's real server with real data. Navigate, resize, hover, screenshot, read the console. Do not click anything that creates, renames, deletes, sends, or archives.
  • Degrade explicitly. When no app is runnable (no free port, no build, a broken dev server, or a profile that says there is none), say so in the report and audit code-only. Never let the browser leg silently vanish, leaving a reader who assumes it ran. A dropped browser leg is a stated coverage gap, listed with the others.
  • Widths. The capture matrix below. Desktop and phone at minimum (1440x900 and 390x844 are where this repo's findings were confirmed); the phone is never optional (charter, 2026-10-18).

Measure the rendered page

Reading the source misses what the browser decides: the touch-target.ts header records four controls under 44px in one phone header, "missed by every review that read the source instead of the page". So the browser leg measures, with one script:

node .agents/skills/auditing-ui/assets/measure/measure.mjs \
  --url http://localhost:<your port> --routes /,/settings --out <scratchpad>/measure \
  --modes base,contrast-more,forced-colors,reduced-motion --focus-walk 12

The capture matrix (its defaults): 390x844, 375x667, 768x1024 and 1440x900, each in light and dark, plus any --modes you name (screenshot-only). States the script cannot reach without a click (hover menus, open dialogs, errors) come from the dev playground's showcases, which render them at rest: point --routes at /dev/<page>, and pass --ignore a selector for the playground's own chrome when it is noise.

What it measures, per base capture, and flags against assets/measure/floors.json:

MeasureRuleFloor (desktop / phone)
Every text run's font-size§2.212 read, 10 absolute / 13 read, 12 absolute
Line height on wrapped and single-line text; tracking under 14px§2.31.3 wrapped, 1.15 single; no negative tracking
Every text field's font-size and height§2.4— / 16px (iOS zooms below it), 44px tall
Every icon's rendered box, by role (inline, control, chrome)§3.112 / 16 / 16 — 14 / 20 / 20
Text contrast against the composited background§4.44.5:1, large text 3:1
The first N tab stops paint the system ring, on screen§7.12px ring
Every interactive element's hit box (a grown ::after counts; inline links exempt)§7.224 / 44
Overlap and gaps between small targets§7.3— / 8px apart
Page-level sideways scroll; text escaping the viewport§8.4, §1.4none

It writes summary.md (counts by severity and rule, worst first), measure.json (every flag with its selector, text, value, floor and where it was seen) and the screenshots, including a crop of each tab stop that fails the ring check. A flag is a candidate, not a finding. Open the screenshot, confirm it, then trace it to the file:line that sets the size (charter validity rule 1) before filing it. A flag the screenshot contradicts is dropped with the reason, like any other dropped finding. Playwright resolves from apps/e2e (@playwright/test) with no setup; PLAYWRIGHT_FROM and CHROME_PATH point it elsewhere.

Scoring. Each confirmed flag becomes a finding in the charter's shape, with the severity floors.json gave it mapped to the charter's rubric (Blocker = P1, Should = P2, Polish = P3) and the rule cited as a section of contributing/design-review.md. The capture matrix and the measured values go in the auditor's coverage note, so the reader can see which viewports and themes were actually looked at.

Cold worktree + browser-leg. Two things the September 2026 run learned the hard way, recorded so the next browser leg does not re-learn them. A cold worktree does not boot the client on @dorkos/shared alone — @dorkos/marketplace, @dorkos/skills, @dorkos/extension-api, and @dorkos/icons dists are needed too; build all of them before booting. And a client on a foreign port talking to the operator's real server sees CORS reject it: API-backed content comes back sparse and a failure toast sits on every page, which starves the states, clutter, and responsive lenses of populated surfaces at render time. Prefer the profile's own dev-server pair (client + server booted together on ports the profile names) over pointing a standalone client at someone else's server.

4. Synthesize

The synthesizer reads the charter and every raw file, then produces one report that stands alone for a reader who never opens the raw files. Template: assets/synthesizer-prompt.md.

Spawn a synthesizer only when two or more raw files exist. On a single-lens run step 1 is vacuous and the runner synthesizes inline — read the raw file, verify its citations, write the report — which is cheaper and identical in output.

  1. Dedup. The same underlying defect seen by several lenses becomes one finding. Keep the best evidence and note which lenses saw it.
  2. Spot-verify citations. Open the cited files: all of them at or below twenty findings, at least fifteen above that. A citation that does not hold kills or narrows the finding. Record what was verified; the count is part of the report's credibility.
  3. Drop the invalid. Findings with no citation, findings relitigating a settled ADR, and recommendations that fight the design language go. Record every drop with its reason: a dropped finding sometimes carries a real observation that deserves re-filing with a proper trace, and the record is how that happens.
  4. Batch by collision class. Group survivors into PR-sized batches, grouped so that two batches worked in parallel touch disjoint files. Same slice or same theme is the usual proxy. Three to fifteen findings per batch on a full run; on a scoped run collision class wins over batch size, and a one-finding batch is a legitimate result — never merge classes that genuinely collide just to reach a floor. Give each batch the priority of its worst finding and an effort mix. Batches that must follow another (copy stragglers after the copy sweeps, file moves last) get that dependency stated.
  5. Be honest in the summary. Say what is good as well as what is wrong, and state plainly how many findings ask for deletion versus addition. Simplify-first is measurable.

5. The ownership contract

Audit findings enter the tracker fenced, so the repo's workflow engine cannot pick them up before a human decides where they belong. This contract is normative and lives only here; the commands point at it rather than restating it.

  • Every /ui-audit:run and /ui-audit:pulse emission creates one per-run type/meta item, "promotion decision for audit run <date>", and every emitted finding-batch item carries a blockedBy edge to it. A source/audit label rides along as provenance only, and is explicitly not a fence. A run that emits no new batches — everything found was a dedup refresh of an already-filed item — emits nothing and creates no meta item.
  • Never write agent/* labels. Those are the workflow engine's durable claim labels; borrowing them makes its own loops treat audit items as orphaned work to re-adopt, which inverts the fence. In-progress visibility uses the plugin-namespace label ui-audit/in-progress (not audit/claimed: Linear enforces team-wide label-name uniqueness, and claimed already exists as agent/claimed).
  • The fence is triple, and needs zero modification to the workflow engine: (1) an open blocker mechanically fails its readiness condition, so triage and groom cannot honestly arm the item; (2) the meta item hits groom's own "meta, never ready" rubric; (3) the dispatch engine independently drops open-blocker items even if one were mis-armed.
  • Promotion is resolving the blocker, and that is the fork. Chosen per batch by the operator, or by the configured default:
    • workflow path — remove the edge; the item enters the normal lifecycle; the audit never touches it again.
    • audit path — /ui-audit:execute works items still blocked (the fence stays up against the workflow engine the whole time) and closes them on merge.
  • No tracker configured? The ledger is markdown: audits/runs/ui/<date>/backlog.md. No contention exists, so no fence is needed; the ledger is the tracker of record.
  • Residual risk, disclosed. Groom could propose closing the promotion-decision meta item as junk. Closures are the one class that always passes groom's itemized human gate with evidence, so the operator catches it there.
  • The meta item has an end of life. Close it once every batch in its run is promoted or closed. Otherwise it becomes exactly the stale ledger groom sweeps at.

How to actually write it

All tracker I/O routes through the /flow plugin's linear-adapter skill (AGENTS.md, and the adapter's own rule that no other skill may touch a tracker string). Load it and use its verbs; do not hand-roll a tracker call from here. Four things it settles, and three traps it documents:

  • The relation verb is link(a, b, type) with type blocks, and direction matters. The fence is link(metaItem, batchItem, 'blocks') — the meta item blocks the batch item, which is what gives the batch item the open blockedBy the readiness check reads. Reversed, the fence is not merely absent, it is backwards, and the meta item becomes the blocked one.
  • Create the labels first. source/audit, ui-audit/in-progress, and the type/meta value must exist in the tracker before an emission references them.
  • Team and project come from the profile, not from this file.
  • Labels read back flattened to leaf names. A grouped label arrives as meta, not type/meta, and as claimed, not agent/claimed. Match on the leaf when reading; never conclude from a bare claimed that a label is in your namespace.
  • A label write REPLACES the entire label set. Compute the union against a fresh read taken immediately before the write. A union computed from an earlier snapshot silently deletes labels a concurrent session added in between, which on this repo has happened live.
  • LINEAR_RUN_QUERY_OR_MUTATION takes query_or_mutation, not query. Passing query fails validation; this is the adapter's own verified-schema trap, not something to rediscover here.

6. Keeping the charter current

The charter accretes; nothing rewrites it wholesale, and /ui-audit:init will not overwrite an existing one. When the operator states a new durable directive, record it the same turn:

  1. Add it to the charter's Standing directives with its date and the operator's own framing.
  2. Sharpen the lens or lenses it binds in the same edit, so an auditor reading only its lens still sees it.
  3. If it is true of every domain and not only this one, it belongs in audits/README.md instead.
  4. If it changes the lens set or a lens key, update the Lens classes section too: the rotation and the stamps are keyed off it.

7. Landing the work

Executing a batch is parallel-batch work like any other, and that playbook is deliberately not duplicated here:

  • orchestrating-parallel-work → its "Landing Parallel Batches" section, which owns sequencing by collision class, the per-batch chain, the landing rules (format last before every push; verify the merge is armed or queued; rebase to both intents; test-merge in-flight branches), rolling dispatch, and close-out discipline.
  • REVIEW.md → its "Failure modes worth hunting by name" section, the library a review brief points a reviewer at. assets/review-brief.md is that brief.

Both sections are on main. If grep 'Landing Parallel Batches' finds nothing in that skill, your checkout predates them and you substitute your own landing method.

8. Design review of one change

A PR review is the smallest scope: one change, judged against contributing/design-review.md, with no tracker emission. /ui-audit:measure is its entry point.

  1. Capture the change, not the app: the routes or playground pages it touches, at the full capture matrix, plus every state the change touches (design-review §0). When the PR only brings screenshots, review those and say which viewports, themes and states are missing; a missing phone capture is itself a finding (§8.1).
  2. Measure with the script, and read every screenshot.
  3. Judge the rest by eye against the unmeasured rules (hierarchy, states, motion, copy, fit).
  4. Sign off in the PR thread in the format at the end of design-review.md: Signed off or Not yet, then Blockers, Should and Polish, each with the issue, the rule, the measured value against its floor, and the fix. One line on what is good.

Assets

  • assets/auditor-prompt.md — the per-lens auditor prompt.
  • assets/synthesizer-prompt.md — the synthesis prompt.
  • assets/review-brief.md — the adversarial-review brief for an execute batch.
  • assets/measure/ — the capture-and-measure script (measure.mjs, with its in-page, focus-walk and report modules) and floors.json, the measured floors keyed to contributing/design-review.md.
  • assets/reference-implementation/ — two non-normative orchestration scripts from the September 2026 run, kept as known-good examples for one specific session orchestrator. The prose above is what is normative; the scripts are color. See that directory's README.

Harness note. This skill is shared (.agents/skills/), and its procedure is written in capability language so any harness can follow it. Three references are unavoidably specific to this repo's Claude Code setup: the /ui-audit:* commands, the orchestrating-parallel-work skill (Claude-only by manifest), and the /flow plugin's adapter. A harness without them still runs §§1-6 and §8 unchanged; for §7 it substitutes its own landing method, and for §5 its own tracker path or the markdown ledger.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.