| id | 213 |
|---|---|
| title | Built-in `no-llm-tells` convention with append-mode forbidden lists |
| status | ✅ |
| model | opus |
| depends-on | |
| summary | Ship a built-in convention that enables MDS055 and MDS056 with curated lists of LLM-writing tells (banned words, phrases, paragraph starts), plus tighter MDS023 / MDS024 thresholds for non-native readers. Switch MDS055 `starts:` and MDS056 `contains:` to append-mode merge so projects can extend the bundle without losing it. |
One config knob — convention: no-llm-tells.
It enables a curated baseline of mechanical anti-slop checks. Three categories: banned vocabulary, banned phrases, banned sentence openers. It also tightens MDS023 and MDS024 thresholds for non-native readers.
docs-author (.claude/skills/docs-author/)
ships a catalog of LLM-writing tells in
slop-patterns.md. The skill applies the catalog
at write or revise time.
Part of that catalog is mechanical. Banned words, banned phrases, banned paragraph openers — the kind of tells MDS055 forbidden-paragraph-starts and MDS056 forbidden-text can already check. A rule catches them in CI and in LSP without a model in the loop.
The cleanest way to ship the mechanical layer is a new built-in convention. The conventions reference already documents the pattern: an opinionated rule bundle behind one config key.
One blocker: MDS055's starts: and MDS056's
contains: are list settings, which replace by
default per
CLAUDE.md's config merge semantics.
A project that sets convention: no-llm-tells and
then adds its own forbidden words would erase the
built-in list. The convention is unusable until
those keys merge by append.
- Detecting structural slop (rule-of-three
garnish, hedging seesaw, meta-narration,
uniform sentence length). Those stay in the
docs-authorskill — they need contextual judgement nocontains:list can express. - Detecting tone slop (promotional voice, obsequious phrasing, padded qualifiers). Same — needs context.
- Auto-generating the convention's lists from
slop-patterns.md. v1 hardcodes the lists in Go; a follow-up plan can wire a generator if drift becomes a problem. - Pinning a Markdown flavor. The convention
declares
Flavor: FlavorNone(or the least-restrictive option — see Open Questions) so projects using GFM or Obsidian can still opt in.
Two rule packages opt their list settings into append-mode merge:
internal/rules/MDS055-forbidden-paragraph-startsreturnsMergeAppendforstarts:.internal/rules/MDS056-forbidden-textreturnsMergeAppendforcontains:.
Both implement rule.ListMerger.SettingMergeMode(key).
The placeholder
vocabulary (MDS023 placeholders:) is the
canonical reference implementation.
After this change:
- A user's
starts:/contains:extends the convention's list rather than replacing it. - Duplicate entries collapse to one (set-style union).
- Order is convention-first, then user (for deterministic diagnostics when more than one pattern matches at the same anchor).
A new entry in
internal/convention/convention.go:
"no-llm-tells": {
Name: "no-llm-tells",
// Flavor: see Open Questions.
Rules: map[string]convention.RulePreset{
"forbidden-text": {
Enabled: true,
Settings: map[string]any{
"contains": llmVocabularyAndPhrases(),
},
},
"forbidden-paragraph-starts": {
Enabled: true,
Settings: map[string]any{
"starts": llmParagraphOpeners(),
},
},
"paragraph-structure": {
Enabled: true,
Settings: map[string]any{
"max-words-per-sentence": 25,
},
},
"paragraph-readability": {
Enabled: true,
Settings: map[string]any{"max-index": 12.0},
},
"descriptive-link-text": {Enabled: true},
},
},The two list helpers live next to the convention
entry. v1 hardcodes their contents. The source is
.claude/skills/docs-author/slop-patterns.md —
its "Vocabulary tells", "Phrasal tells", and
"Sentence openers" sections.
For v1, slop-patterns.md and the convention
helpers carry parallel lists. A short comment
above each helper points readers to
slop-patterns.md; a short note in
slop-patterns.md points back. A drift-checker
test (see Tasks) asserts the rule lists are a
subset of the skill catalog so the two cannot
silently diverge.
- Flavor pinning. Built-in conventions all pin
a flavor today. Anti-slop is renderer-agnostic;
pinning would force any user of this convention
off GFM. Options: (a) leave the convention's
Flavorunset and teach the loader to allow that; (b) pincommonmarklikeplainand document the trade-off; (c) ship one variant per flavor (no-llm-tells,no-llm-tells-gfm). Default: (a). User to confirm. - Convention name. Working title is
no-llm-tells. Alternatives:plain-prose,clear-prose,low-slop,house-voice. User to confirm before the plan moves to 🔳. - Default severity. MDS055 / MDS056 emit Error today. Should the convention preset them to Warning, given the heuristic nature of slop detection? Default: leave at Error to match the other rules; projects can downgrade per rule.
- MDS055 / MDS056 list-merger. Implement
SettingMergeMode("starts") == MergeAppendon MDS055 andSettingMergeMode("contains") == MergeAppendon MDS056. Add a failing unit test asserting that a layered config (kind + override) unions the lists rather than replacing. Make it pass. - Convention entry. Add
"no-llm-tells"toconventionsininternal/convention/convention.go. Add a convention-loader unit test that loads it, confirms the right rules are enabled, and confirms the lists deep-merge with a user's own forbidden entries. - Forbidden lists. Add the curated word /
phrase / opener lists as package-level
functions next to the convention entry. Source
from
slop-patterns.md. - Drift-checker test. Integration test that
reads
.claude/skills/docs-author/slop-patterns.md, extracts the items under "Vocabulary tells", "Phrasal tells", and "Sentence openers", and asserts the convention's lists are a subset of each. Fails CI when one source drifts from the other. - Docs. Add a
no-llm-tellssection todocs/reference/conventions.mdmatching the format ofportable/github/obsidian/plain. Add a short pointer fromslop-patterns.mdback to the convention. - Skill update. Add a one-paragraph note to
.claude/skills/docs-author/SKILL.mdexplaining that the mechanical layer ships as theno-llm-tellsconvention, and thatmdsmith checkenforces it without the skill in the loop. The skill still owns the structural / tone / formatting passes.
-
convention: no-llm-tellsin a project's.mdsmith.ymlenables MDS055, MDS056, MDS024, MDS023, and MDS063 with the documented settings. - A project that also sets
rules.forbidden-text.contains:with extra strings unions its list with the convention's list; neither side is dropped. - Same for
rules.forbidden-paragraph-starts.starts:. - The drift-checker test fails when an item is removed from either source.
-
mdsmith check .against a fixture with "delve" or "it's important to note that" produces an MDS056 diagnostic. (MDS056 is case-sensitive; the catalog form is lowercase.) -
mdsmith check .against a fixture starting a paragraph with "Certainly," or "Moreover," produces an MDS055 diagnostic. - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues. -
mdsmith check .passes.