Strip AI writing tells from Rove's user-facing prose — README, the docs/ pages that sync to docs.rove.run, landing copy, and release notes. Use when the user says "unslop", "洗一下文案", "这读起来像 AI 写的", or after drafting any user-facing page. Adapted for this repo from cursor/plugins pstack/skills/unslop.
Installs into .claude/skills of the current project.
Are you the author of Unslop?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/sma1lboy-unslop-rove)
---
name: unslop
description: Strip AI writing tells from Rove's user-facing prose — README, the docs/ pages that sync to docs.rove.run, landing copy, and release notes. Use when the user says "unslop", "洗一下文案", "这读起来像 AI 写的", or after drafting any user-facing page. Adapted for this repo from cursor/plugins pstack/skills/unslop.
metadata:
internal: true
---
# Unslop
Rewrite text so it stops reading as machine-generated. Two halves, both required:
cut the tells, then make sure something human is left.
Upstream pattern list: <https://github.com/cursor/plugins/blob/main/pstack/skills/unslop/SKILL.md>.
This file keeps the repo-specific parts — what counts as prose here, what is
data and must survive, and which surfaces are in scope.
## Scope in this repo
In scope (the product's public face):
- `README.md`
- The `docs/` pages listed in `SECTIONS` in `packages/rove-docs/scripts/sync-docs.mjs`
(they publish to docs.rove.run)
- `packages/rove-landing/**` — English copy only
- Release notes and changeset bodies
Out of scope unless asked: `docs/PLAN.md`, `HARNESS.md`, `ARCHITECTURE.md`,
`DESIGN.md`, `RELEASING.md`, `AGENTS.md`, and `docs/design/**`. Those are
operator manuals for agents, not product surface.
## Never rewrite these
The same character can be punctuation in one line and data in the next. Check
before replacing:
- **Table cells meaning "not applicable"** — `| copilot | — | ... |` in
ENGINES.md and SESSIONS.md. An em dash there is the value.
- **Verbatim CLI output** — e.g. the `rove doctor` sample in TROUBLESHOOTING.md.
Quoting output means quoting it exactly.
- **Code, including placeholders** — `${ev.taskId ?? "—"}` in PLUGIN-SDK.md.
- **CSS/JS/HTML comments** in landing files.
- **Chinese copy.** `——` is correct CJK punctuation, not a tell. The landing
page's `zh` i18n dictionary keeps its em dashes.
- **A file's own established separator.** PLUGIN-EVENTS.md uses `·` between a
heading and its engine-support marks; match the file rather than inventing
a new convention.
## The patterns
Full list upstream. The ones that actually show up here, in order of volume:
1. **Em dashes in prose.** By far the biggest. Resolve each one *per sentence*
into a period, comma, semicolon, colon, or parens. Do not global-replace
with a single substitute — swapping every `—` for `,` just trades one tic
for another, and swapping for parens trades it for a different one.
2. **Inline-header lists.** `- **Label** — restatement of the label` becomes
`- **Label.** <genuinely new detail>`. If the text after the label only
rephrases it, the line has no content; write the content.
3. **Curly quotes** → straight.
4. **Filler.** "simply", "in order to", "it is important to note that".
5. **Fancy ways to say "is"** — "serves as", "stands as", "boasts".
6. **Title-case headings** → sentence case. Proper nouns and defined product
terms (Terminal Tab, Claude Code) stay capitalized.
7. **Sentences that say nothing specific to this project.** If the line could
appear verbatim in another tool's docs, cut it.
Then add voice back: vary sentence length, state the tradeoff instead of
listing both sides neutrally, and let a sentence be blunt.
## Two failure modes this repo has actually hit
Both produced broken output that looked fine at a glance. Check for them.
**Fragments from over-eager splitting.** Turning `X — y` into `X. y` mid-clause
leaves a sentence starting lowercase, or a verbless remainder. After editing,
scan for it:
```bash
python3 - <<'PY'
import sys
for p in sys.argv[1:]:
lines = open(p, encoding='utf-8').read().split('\n')
for i in range(len(lines) - 1):
a, b = lines[i], lines[i + 1]
if a.rstrip().endswith('.') and b[:1].islower() and b.strip() \
and not b.startswith(('|', '-', '*', '`', '#', ' ')):
print(f"{p}:{i+1}: {a[-50:]} || {b[:50]}")
PY
```
Also read each rewritten sentence whole. The scan only catches line-broken
prose, not a fragment that fits on one line.
**Escaping breaks in i18n strings.** The landing dictionaries are single-quoted
JS. Replacing `agent’s` with a literal `'` breaks the file. Always:
```bash
node --check packages/rove-landing/index.js
node --check packages/rove-landing/themes.js
```
## Verify before claiming done
```bash
bun run lint
bun run --filter @sma1lboy/rove-docs build # the docs-site CI gate
node --check packages/rove-landing/*.js
```
Then confirm the em dashes that remain are all data, not prose:
```bash
grep -rn "—" README.md docs/*.md packages/rove-landing/*.html
```
Every hit should be a table cell, CLI output, a code placeholder, a comment,
or Chinese. If one is a sentence, it was missed.
Editing the landing page's English also means checking its Chinese counterpart
still has the same keys:
```bash
node -e '
const s = require("fs").readFileSync("packages/rove-landing/index.js", "utf8")
const keys = (b) => new Set([...b.matchAll(/'"'"'([a-zA-Z0-9._]+)'"'"':/g)].map(m => m[1]))
const zh = keys(s.split("var en =")[0].split("var zh =")[1])
const en = keys(s.split("var en =")[1].split("var dicts")[0])
console.log("zh only:", [...zh].filter(k => !en.has(k)))
console.log("en only:", [...en].filter(k => !zh.has(k)))
'
```