For AI agents: the documentation index is at /taste-lint/docs/llms.txt. Append .md to any page URL, or send Accept: text/markdown, for the Markdown version of that page.
For AI agents: the documentation index is at llms.txt. Markdown versions are available by appending .md to any page URL, including this page's markdown.
Usage
CLI options, findings, rule packs, rendered mode and how a rule gets promoted.
Example findings
PASS: 144 active rules in data/rulessrc/Orders.tsx[MINOR] motion-transition-all (p=1.00) src/Orders.tsx:9:1 transition-all animates every property, layout included transition-all Fix: Name the properties: transition-transform, transition-opacity or transition-colors.[MINOR] copywriting-click-here-links (p=1.00) src/Orders.tsx:13:1 Link text that names nothing Fix: Name the destination or the outcome.2 review notes from 2 rules (never fail a run; use --verbose to list): copywriting-vague-error 1, interaction-no-error-state 1Units: 6 | Rules: 130 | Act: 2 | Review: 2 | Unknown: 0Jev: 2 requests, 0 cached answers, 3120 input tokens, $0.0001FAIL - 2 rule findings meet the failure threshold
Performance: blocking sync calls in request handlers, independent awaits run in sequence, formatters rebuilt every render, heavy libraries with native replacements.
Data access: a query per item in a loop, list reads with no limit, related writes with no transaction.
Test value: tests that assert nothing, compare a value with itself, check only that a mock was called, render and check presence, or snapshot a change detector. Focused and skipped tests.
Test gaps: modules whose branches the tests mostly never run, where the logic can reach users or data. Reads coverage/coverage-summary.json, so run your tests with the json-summary coverage reporter first; without a report the rule reports unknown. CLI entry points and commands/ folders are skipped, because tests that run the binary are invisible to coverage.
Dead code: commented-out code, branches that can never run, flags hard-coded to one value, dependencies nothing imports.
Code quality: files over 1000 lines, blocks repeated in one file, type bypasses, rethrow-only catches, comments that restate the code, placeholder residue.
Telemetry: errors dropped in an empty catch, failures turned into a default with no record.
Sensitive data: committed keys, credentials or personal data in logs, tokens in localStorage, Math.random for secrets.
CI: pull request runs that are never cancelled or that cancel default-branch runs, full clones nothing reads, uncached installs, coverage collected on every pull request.
Tests are in scope for the rules about tests and out of scope for the rest. Minified bundles and files marked as generated are skipped. A whole file too long for one question is judged on the lines around the match.
Two scans are not lint rules. Migration planning traces flows across a codebase to write a plan: use the planning skill. Compliance against your organisation’s policy needs that policy: write it as a rule pack (below), one question per requirement.
--since keeps whole files for context and reports only findings on changed lines.
In GitHub Actions, upload the SARIF so findings land on the pull request diff. Mechanical rules need no key; add AI_GATEWAY_API_KEY as a secret to run the Jev questions too.
28 rules are active and fail a run. All of them are mechanical: a regex, a parsed file or a measured value decides. The other 177 are review-only:
Pattern ports from ui-design, ui-animation and typography-audit that match raw source. A match nominates a candidate; it does not prove the skill’s conclusion.
Jev questions, one per rule, that need a labelled corpus before they may block.
A Jev rule is promoted only by taste-lint tune --write, and only when the fixed threshold clears the precision lower bound on an independent holdout. No rule has been promoted yet. The loudest two were measured at 0 to 10 percent precision on real prose, which is why review notes are counted rather than listed.
Promoting a rule
A judge nobody has checked settles hard cases with the model’s biases. Work this loop in order and repeat it; human time comes last, spent only on what the AI loop can’t settle.
Get it working.taste-lint eval should report Jev: N requests with no errors. A rejected key fails the run instead of reporting a judge that never fires.
Find failure modes.taste-lint eval --only <rule> --split dev --disagreements lists every row where the judge and the label disagree at the review threshold. Group them by cause first: the judge was never asked, the rubric reads plain text as a violation, or the label is wrong.
Fix them with AI. Have an agent rewrite the rule’s question from the dev disagreements. Confirm each wording with taste-lint tune ab --rule <rule> --variant <file>, then check the holdout with eval --split holdout. Each edit should be a general policy, not a patch for one row.
Then optimise cost, then speed: gates, preconditions and batching, measured with the same eval.
Then label the residue yourself. Export samples with lint --samples results/blind.json (likely failures first), label them with taste-lint eval label results/blind.json --minutes 120, list rubric gaps with eval gaps, and import with eval labels results/blind.json --out data/corpus/<name>.jsonl. tune --source hand --write then promotes the rule, or leaves it review-only, on the untouched holdout.
To reuse an AI labeller, label the same samples both ways and run taste-lint eval agreement results/blind.json results/ai.json. Trust its labels only for rules where the agreement lower bound clears the floor.
A wrong finding is corpus evidence. Add the unit with the correct label rather than lowering a threshold by hand.
Rule packs
Typography: straight quotes, dashes, ellipses, primes and units from typography-audit. Size, weight, tracking and line-height checks over resolved Tailwind classes or computed styles.
Copywriting: claims without evidence, vague errors, friction CTAs, hedges and register shifts from ghostwriter and the ui-design copy guideline. The ghostwriter structure tells block at one sighting: participle tails (“, ensuring…”), antithesis (“it’s not X, it’s Y”) and tee-ups (“Here’s the thing”). A quoted example is never a tell. Every’s AI-tell checker contributes 19 questions, one rule each.
Interaction and craft: the static checks of ui-design/rules as whole-file patterns. Focus traps, error and empty states, target size, i18n, lazy loading. The shadcn/lint class hygiene rules cover raw palette colours, arbitrary values and interpolated class strings.
Motion and product: the ui-animation flag-on-sight table, transition: all in CSS, will-change misuse and framer-motion imports (use motion/react), plus the two deterministic product-design rules.
Every rule names the file and line of the skill or lesson it came from. taste-lint rules list prints tier, status and category per rule.
Rendered mode
taste-lint lint --url https://example.com/pricing --selector main
Runs style-capture in headless Chromium and lints computed styles. The typography rules then judge what the reader sees rather than what the class list implies. --selector narrows the page.
API
import { runLint } from "taste-lint";const result = await runLint({ root: process.cwd(), profile: "product", targets: ["."],});console.log(result.summary, result.usage.costUsd);
runLint takes the same options as the lint command, all optional. Pass evaluate in the second argument to supply your own judge. taste-lint schema prints every command, flag and default as JSON. --output json turns an error into a { error, code, message } envelope on stdout.
Options
Flag
Default
Description
--profile <name>
product, writing, instructions, code or all; paths default to .
--dry-run
Plan and print units, requests and estimated cost without calling Jev
--since <ref>
Report only findings on lines changed since a Git revision
--staged
Lint the files staged in Git, as staged. Takes no paths
--no-error-on-unmatched-pattern
Pass when no file is selected, and skip missing or unsupported paths
--only <ids>
Comma-separated rule ids
--brief <file>
What the product is and does. Copy that claims anything else is flagged by copywriting-invented-capability
--exclude <globs>
Comma-separated globs to skip, added to taste-lint.config.json
--fail-on <severity>
minor
Lowest severity that fails the run
--fix
Apply deterministic fixes to act-band findings
--samples <file>
Write blind labelling samples for the Jev rules that ran
--output <format>
tty
tty, json or sarif
--verbose
List review notes, suppressed findings and unknowns
--url <url>
Lint a rendered page through style-capture
--help lists the rest: --root, --results-dir, --model, --no-cache, --progress, --selector and --print-requests. Every run saves a complete JSON report and prints its path. An incomplete run prints a retry command that reuses successful answers.
Configuration
Optional, at taste-lint.config.json in the project root. The editor schema ships at node_modules/taste-lint/data/config.schema.json. Unknown fields and invalid types fail before evaluation.
rules adds rule directories laid out like data/rules: domain folders of YAML rules. Paths resolve from the project root; ~ and $VARS expand.
Suppress one unit with taste-lint-ignore: <rule-id> on the line above. Projects that curl quotes at build time are detected; force it with smartQuotesAtBuild.
Voice pack
A voice pack is a rules directory that checks one person’s or company’s voice. The ghostwriter profiles ship one at $GHOSTWRITER_HOME/taste-lint: Australian spelling, the ChatGPT link tag, unicode arrows, em dashes, stock email sign-offs, LinkedIn bait, blog exclamation marks and Slack hype, each citing its profile line.
{ "rules": ["$GHOSTWRITER_HOME/taste-lint"] }
Rules scope themselves by glob, so a draft named intro.email.md gets the email checks and blog/post.md the blog ones. --fix rewrites US spelling and strips ?utm_source=chatgpt.com. Pack rules are mechanical: no text is sent anywhere.
Pre-commit hooks
A hook runs the mechanical checks on what is being committed and blocks the commit on an act finding. Add --dry-run so it never calls Jev: no key, no network, no bill. init --hook writes one of these for you.
--staged reads Git’s index itself, so it works from any hook. It lints each file as staged: a file with only some hunks added is checked as it will be committed, not as it sits in the working tree.
The same goes for lefthook’s {staged_files}. --no-error-on-unmatched-pattern is the flag oxlint and oxfmt use for the same job. Without it, a run that selects no files fails with exit code 2, since a scan of nothing is not a pass. With it, files that taste-lint.config.json, .gitignore or the profile leave out are skipped, as are paths that no longer exist or that taste-lint does not read. Keep excludes in taste-lint.config.json; the hook does not need its own copy.
To fix as well as check, run --fix with lefthook’s {staged_files} and stage_fixed: true, or from lint-staged, which stages the fixed files. --fix cannot be combined with --staged, because it writes the working tree. To adopt the hook on a codebase with old findings, add --since HEAD: it reports only lines this commit changes.
Project setup
Run npx taste-lint@latest init in a directory with a package.json. Setup detects the package manager, installs Taste Lint as a development dependency, and adds a taste script. React, Next.js, Vue, Svelte and Astro dependencies select the product profile. Other projects start with writing.
--root <path> selects a project directory, including one workspace package.
--pm npm|pnpm|yarn|bun overrides detection.
--dry-run previews setup without writes or installation.
--no-install adds the script without running the package manager.
--agent appends a marked section to AGENTS.md once.
--hook lefthook|husky|lint-staged adds the pre-commit hook, installs the runner if it is missing and activates it. Existing configuration is merged, and a second run changes nothing. Without --hook, setup names the runner it finds.
Setup never asks for or stores an API key.
See Design for the contracts and TypeSafe for the Jev integration.