Turn a prose rule that
no off-the-shelf linter rule matches into a custom
lint rule — the opt-in
synthesis step. This is the hand-off target of the
custom rule (⚙) lane in a
rule map: audit maps a rule there
when it looks enforceable but nothing off-the-shelf fits. You invoke this skill
explicitly; nothing here runs on its own, and installing vigiles never starts
synthesizing anything.
The point of this skill is not "write a checker." A model can write a
plausible-looking checker in seconds. The point is the discipline that makes the
result trustworthy: synthesize the rule and an independent test that
encodes the rule's real intent, run the checker against adversarial cases it
didn't author, and abstain — hand it back as prose — if it leaks. A checker
that matches a rule's surface but not its intent gives false confidence
(the measured failure mode: most naively-synthesized checkers silently leak).
Shipping a green check nobody should trust is worse than shipping nothing.
Arguments
— a prose rule to enforce. Either a
custom rule (⚙) line copied
from a
report, or free text. Examples:
- "we keep telling people not to import directly from antd — use our design-system barrel"
- "people forget to use our custom logger instead of console.log"
- "wrap outbound API calls in our helper"
- "route handlers must go through the wrapper"
The pipeline
prose rule
→ detect language + linter (ask if ambiguous)
→ REUSE first: does an off-the-shelf rule already fit? → use it, STOP (no synthesis)
→ clarify intent + read the codebase (what exactly is the violation? the fix?)
→ SYNTHESIZE: the rule + an INDEPENDENT intent-encoding test (adversarial cases)
→ TRUST GATE: run the checker on that test; precision AND recall must be 1.0
pass → KEPT (safe to enforce; wire it in)
leak → ABSTAIN (hand back as prose; never ship a checker it can't prove sound)
Instructions
Step 1 — Detect the language and toolchain
Read the repo to find the primary language, the linter in use (ESLint,
Ruff, Pylint, Clippy, RuboCop, Stylelint…), the test framework, and any
existing custom rules (to match conventions). If you cannot confidently pick
one — polyglot repo, no linter config, several candidates — ask the user
before generating anything.
Step 2 — REUSE before you synthesize (ADOPT > REUSE > SYNTHESIZE)
Synthesis is the last resort, not the first. Before writing any rule:
- Check existing plugins / built-in rules. Read the linter reference doc
below (the plugin table + families) — most one-off
prohibitions are a config line, not custom code.
- Try a built-in construct rule. ESLint /
, Ruff/Pylint selects, RuboCop cops, etc.
- Only synthesize when the rule needs AST analysis, a fix, or options that
no existing rule provides.
If an off-the-shelf rule fits,
use it and stop — enable it (one config line)
and jump to Step 6. That is the
outcome, and it's the better one;
don't synthesize a rule when a maintained one exists.
| Language | Linter | Reference doc |
|---|
| JavaScript/TypeScript | ESLint | ../../../skills/linter-docs/eslint.md
|
| Python | Ruff | ../../../skills/linter-docs/ruff.md
|
| Python | Pylint | ../../../skills/linter-docs/pylint.md
|
| Ruby | RuboCop | ../../../skills/linter-docs/rubocop.md
|
| Rust | Clippy | ../../../skills/linter-docs/clippy.md
|
| CSS | Stylelint | ../../../skills/linter-docs/stylelint.md
|
For
Go, generate a
golang.org/x/tools/go/analysis
analyzer with
tests. For any other language, use the most idiomatic linting
approach with test cases and integration notes.
Step 3 — Nail the intent, then read the codebase
Synthesis fails when the checker enforces what's easy to match instead of what
the rule means. Before writing anything, pin down:
- The exact violation. "No hardcoded secrets" is not "a variable named
" — it's , , an env-less literal too. "Wrap API calls
in retry" needs the real signature of the calls and the wrapper.
- The fix. What does the compliant form look like, concretely, in this
codebase? Read the real files — the helper's import path, its call shape.
- The clean lookalikes. What looks like a violation but isn't? (the pattern
inside a string, a comment, a JSDoc ; an aliased/namespaced form.)
If any of this is ambiguous — and for anything like "wrap API calls in retry" it
usually is — ask the user rather than guessing. A rule built on a guessed
intent will fail the gate anyway; asking is cheaper.
Step 4 — Synthesize the rule AND an independent intent test
Write two artifacts:
-
The rule — prefer AST/keyword analysis over text/substring scanning
(text-scan checkers false-positive on the pattern appearing in prose; a
name-based checker false-negatives on synonyms). Follow the reference doc's
anatomy, fix/suggest, and registration guidance.
-
An independent test that encodes the INTENT, not the checker's shortcut.
This is the trust anchor — write it to the rule's meaning, then seed it with
the adversarial cases that break naive synthesis:
- valid (must NOT flag): the pattern in a string literal, a line comment,
a JSDoc/docstring ; a camelCase identifier that merely contains
the keyword (); a compliant call that looks close.
- invalid (MUST flag): every real form of the violation — synonyms
(/, not just ), the aliased/namespaced form
(), the wrapped-differently case.
Do not make the test agree with a shortcut the checker took. If the rule is
"no hardcoded secret," the test asserts
is a violation even if your
first checker only catches
— so the gate can catch the leak.
Step 5 — The trust gate: precision AND recall must be 1.0
Run the synthesized checker against the Step-4 test (ESLint
, pytest,
, whatever fits the linter).
- Pass (every invalid flagged, every valid clean → precision = recall = 1.0):
the rule is KEPT — safe to enforce. Continue to Step 6.
- Leak (any false positive or false negative): the rule is ABSTAINED.
Do not wire it in. Report honestly why it leaked (e.g. "matches the
pattern in a comment," "misses ") and either (a) hand the rule back as
prose, or (b) if you can fix the checker, do so and re-run the gate — but never
ship an un-passed checker.
The reference gate lives in
(two-stage: self-test + a blind
adversarial gold set), the canonical adversarial cases in
rule-enforcer/gold/gold.json
,
and a runnable end-to-end demo (
cd rule-enforcer && npm run demo
— watch it keep the
sound rules and abstain the two leaky ones). Read them for the case shapes and the
abstain discipline, then mirror it here; don't reinvent the gate.
Step 6 — Wire a KEPT rule into the instruction file
Only for a rule that passed the gate (or an off-the-shelf rule reused in Step 2).
v2 specs (repo has
): add an
line and
recompile —
typescript
"<rule-id>": enforce("<linter>/<rule-name>", "<why>"),
v1 (hand-written
): append —
markdown
### <Rule title — imperative, concise>
**Enforced by:** `<linter>/<rule-name>`
**Why:** <one sentence — the architectural reason>
Step 7 — Present the outcome
Show the user, honestly:
- Kept rules: the generated files, the gate evidence (the adversarial cases
it passed), integration steps, the spec/ block, and how to verify
(run the linter, watch it catch a real violation).
- Abstained rules: which rule, why it leaked, and that it stays prose — a
refused checker is a correct outcome, not a failure to hide.
Then ask before writing any files or editing the spec/
. Nothing is
written for the user automatically.