Python CLI Agent skill GitHub Action MIT

Measure prose against an author's voice, and restyle a Markdown book toward it.

mdrestyler builds a voice profile from DRM-free EPUBs or from an author's own Markdown book. It checks drafts against that profile with narration and dialogue measured apart, runs a copy guard so a draft that repeats a scanned book is caught, ranks several takes of one unit, and tables a whole book unit by unit. Its agent skill runs a need-first restyle: assess every unit, restyle only those that need it, pilot first, and check that nothing but the voice changed.

For writers, editors and authors working on a Markdown book, and for the AI coding agents that help them. The measures locate drift. A reading decides: mdrestyler never picks a take or accepts a change by the number.

Install with pip

Python 3.10 or later. Standard library only, no dependencies.

pip install mdrestyler
mdrestyler --version
mdrestyler --help

The wheel is also attached to each GitHub release. --version prints the version, the source commit and the manifest digest.

Use it as an agent skill

The skill folder runs with nothing installed. Copy skills/mdrestyler/ into your agent's skills folder; from inside it the command is python3 scripts/mdrestyler.py.

git clone --branch v0.1.1 https://github.com/sagol/mdrestyler.git
cd mdrestyler
python3 skills/mdrestyler/scripts/mdrestyler.py --help
python3 skills/mdrestyler/scripts/mdrestyler.py verify

verify checks the skill folder against its manifest. The agent reads SKILL.md and its references before it acts.

Run it in GitHub Actions

A composite action that runs one mdrestyler command with the runner's Python.

- uses: sagol/mdrestyler@v0.1.1
  with:
    command: check
    args: --profile voice chapters/01.md --strict

Inputs: command (default check), args (one string, split like a shell would) and working-directory (default .). The step exits with the command's status, so a copy-guard hit fails the job. See action.yml.

examples/cross-restyle exit 0
# Huck Finn, restyled toward The Turn of the Screw
$ mdrestyler check --profile profiles/james \
    restyle/twain-to-james/take-2.md
Voice check
Language: en; words: 1519
Profile status: draft (not yet approved: …)
Copy guard: clear
Voice distance: 1.04 over 29 measure(s); closer to the
  profile than 36% of its 22 source chapters

| Measure                   | Draft | Target range  | Status  |
| commas per 1000 words     | 77.7  | 69.5 to 96.0  | within  |
| semicolons per 1000 words | 13.2  | 4.571 to 10.0 | outside |

Narration only (1413 words; voice distance 0.98,
  closer to the profile than 55% of its 22 source
  chapters' narration)

# Excerpt. The full report: restyle/twain-to-james/check.md
What it measures

A check reports where a draft departs from a voice

Every report names the draft's SHA-256 and the profile's revision (the hashes of its three files), so a report is stale for any other revision. Then it gives the profile's status and the findings below.

Targets

Each measure against its range

Sentence and paragraph length, punctuation per 1,000 words, mean word length, vocabulary variety (MATTR), -ly adverbs, sentence openers, pronouns, past-tense share, dialogue share and speech tags. Each is within, near, outside or not measured, and marked weak when the draft is too short for it.

Voice distance

Distance from the profile's sources

When the profile holds a calibration: the draft's mean robust distance from the source chapters, and the share of those chapters that lie farther from the rest. A draft farther than every source chapter is a finding. Under 1,200 words it reads less reliably.

Narration / dialogue

The author's words and the spoken words apart

Each register is measured against its own ranges, and gets its own distance when the profile calibrates it and the draft holds 600 words of narration or 400 of dialogue. The narration distance leaves dialogue out, so it shows best what rewording can move.

Copy guard

Runs shared with a scanned book

Any passage of fifteen words or more that a draft shares with a scanned book is found when it runs unbroken as a reader sees it, with markup removed, or as raw text. A hit exits 1 and blocks the draft.

Revisions

A revision against its accepted base

With --accepted base.md, text the revision keeps unchanged, in place and unambiguous counts as the base's own. Repeated base text is a hit at every copy, moved text is uncertain, and inserted text stays checked.

Takes and books

Rank takes, table a book

rank orders two or more takes of one unit. book gives one row per unit in reading order, with a last column that reads the narration distance against a threshold: beyond, within or by reading. Both order a reading; neither decides.

Read by the narration distance. The pooled distance also weighs how much of a unit is dialogue, which follows composition and length more than voice. In one test, on a chapter of a narration-led novel against a dialogue-led reference, a restyle moved the narration distance from 1.21 to 0.93 while the pooled distance went from 1.15 to 1.14.

How a restyle works

Need first, pilot first, one whole take by reading

A restyle rewrites chosen units of a book toward a profile and keeps everything but the voice. It spends takes only where the voice really differs, and nothing replaces accepted text before the user accepts it. The skill's restyle procedure has ten steps, with every record kept outside the manuscript in a run folder such as restyle/run-1/.

  1. Decision note

    The approved profile with its three hashes, the units in scope with their SHA-256, the threshold (p90 by default), the pilot, the preserve list and who accepts. The book's owner confirms it before any take.

  2. Before table

    book tables every unit in scope against the threshold, as text and as JSON.

  3. Need assessment

    Read every unit against the described voice, beyond-threshold units first. Record each as needing a restyle, naming the difference, or not, with its distances and hash. If no unit needs one, the run ends here.

  4. Pilot

    Restyle one unit that needs it and show it. Other units wait until the pilot is accepted. A rejected pilot changes the profile or the decision note, never the preservation rules.

  5. Each unit

    Save the accepted text as snapshot.md, write three takes from the same reading, rank them, read every take and choose one whole. Write why in choice.md. Never splice takes.

  6. Preservation

    Side by side with the snapshot: events and their order, facts, numbers, names, each dialogue line's speaker, act and meaning, what each character knows and when, and scene boundaries. A meaning change with unchanged numbers is a failure.

  7. Bounded checks

    check --accepted snapshot.md for the copy guard and the narration measures, a prose check on every rewritten sentence, and a seam check at both joins. A guard that cannot run blocks promotion.

  8. Promotion

    Read the candidate back, diff it against the snapshot, record the acceptance, and only then replace the accepted text.

  9. Resume

    Each unit is classified, in order, as unknown, stale, done, promoted, pending or conflict. Only pending units continue to takes; a conflict is reported and nothing changes.

  10. After table

    Run the before commands again with the same --within. The tables locate change; the user's reading accepts it.

Commands a restyle runs

mdrestyler book my-book --profile my-book/voice --list units.txt --within p90 > restyle/run-1/before.md
mdrestyler book my-book --profile my-book/voice --list units.txt --within p90 --json > restyle/run-1/before.json
mdrestyler rank --profile my-book/voice take-1.md take-2.md take-3.md
mdrestyler check --profile my-book/voice take-2.md --accepted snapshot.md

Drafting outside a restyle writes one take. A final draft, and each unit of a restyle, gets three independent takes, in fresh contexts where possible. Three takes cost about three drafts' tokens. See drafting toward a profile.

Worked example

Huck Finn and the governess, restyled into each other's voice

The repository's cross-restyle example takes two public-domain passages, a passage of Adventures of Huckleberry Finn (Mark Twain, 1884) and one of The Turn of the Screw (Henry James, 1898), and restyles each toward the other's voice. It follows the restyle procedure from start to finish: a profile for each voice, a decision note, a need assessment, three takes per direction, a ranking, a choice made by reading, a preservation check, and before and after tables.

The two voices

Twain: a boy telling his own story in vernacular. Short words, nonstandard grammar, clauses chained with and and semicolons, almost no -ly adverbs, deadpan.

James: a governess writing years later. Long suspended sentences full of qualifications, abstract and Latinate words, many -ly adverbs, analysis of every impression.

Both books are narrated in the first person and the past tense, so a restyle can keep the viewpoint and change only the voice. The passages are the opening of chapter VII of Huckleberry Finn, where Huck fakes his own murder (1,402 words), and the second half of chapter III of The Turn of the Screw, where the governess first sees a stranger on the tower (1,297 words).

How the takes were made

Three separate agents per direction each wrote one take, given only the snapshot, the target profile and its narration ranges, with the preservation rules and no measuring tool. Both profiles were scanned from their books as reference books, and both are drafts: no author approved them, which a real restyle requires.

For the escape passage the reading chose the take the ranking put first. For the tower passage it chose the second: the first-ranked take kept the source's long paragraphs, so the vernacular sat inside the governess's paragraph shape.

Narration distance from each profile. Lower is closer; within and beyond read the distance against the 90th percentile of the profile's own chapters (Twain 1.18, James 1.17).
Text Twain profile James profile
Twain passage 0.84 within 1.63 beyond
Twain passage restyled toward James 1.20 beyond 0.98 within
James passage 2.31 beyond 1.17 within
James passage restyled toward Twain 1.11 within 1.34 beyond

Each restyle crosses over: the text moves inside the target's range and out of its source's. The escape passage, restyled, lies nearer the James profile than 55% of the James book's own chapters.

Twain, chapter VII (source)

He unlocked the door, and I cleared out up the river-bank. I noticed some pieces of limbs and such things floating down, and a sprinkling of bark; so I knowed the river had begun to rise. I reckoned I would have great times now if I was over at the town.

Restyled toward James (chosen take 2)

He unlocked the door, and I took myself off up the river-bank. I noticed, as I went, certain pieces of limbs and such things floating down, together with a sprinkling of bark; and from these I knew, beyond any doubt, that the river had begun to rise. I reckoned, as I remember, that I should have great times of it now if only I had been over at the town.

What moved in the narration

  • Toward James: commas per 1,000 words rose from 49 to 80, into the target range; -ly adverbs from 1.5 to 8.5, still below the target's 12 to 23; long sentences from 20% to 32%; the past-tense share from 0.91 to 0.99. Mean word length barely moved (3.75 to 3.79), because the events are told in plain words and a restyle may not add new matter to say in longer ones.
  • Toward Twain: -ly adverbs fell from 11.6 to 2.0 per 1,000 words, dashes from 17 to 7, vocabulary variety from 0.49 to 0.45 and the median paragraph from 338 words to 87, all into or near the target ranges. Sentences stayed more evenly long than the target's (variation 0.45 against 0.68 to 1.16): the governess's qualifications had to stay, and they keep the sentences long.
  • Pooled against narration: the tower passage restyled toward Twain has a narration distance of 1.11 but a pooled distance of 1.71. The Twain book is a third dialogue and the passage has none; rewording cannot change that.
  • The copy guard sees the source. The chosen takes still share 66 and 4 runs with their source books, mostly lists and short phrases a restyle keeps. In a real restyle the check runs with --accepted snapshot.md. Against the target's guard, every take is clear.

Run it yourself

cd examples/cross-restyle
python3 run.py            # regenerate the reports under restyle/ and results/
python3 run.py --check    # exit 1 if a committed report is stale
Reference

Commands, flags and exit codes

Installed from the wheel, the command is mdrestyler. From the skill folder with nothing installed it is python3 scripts/mdrestyler.py, with the same commands. Each command's --help says more.

mdrestyler commands with their key flags and exit codes
Command What it does, key flags Exit codes
scan Builds a voice profile, one per language with enough text, from --own EPUB and --reference EPUB (each repeatable), or from --own-book BOOK --eligibility FILE. Writes the profile to --out and sample passages to --work (both required, both outside any book). --min-words (default 3000), --replace (old files move to history/), --max-expanded-mb (default 64). 0 built · 1 refused (DRM, a size limit, an existing profile, a folder inside a book, a destination that is a symbolic link, the same book twice) or no language had enough text · 2 error (a damaged archive, an input that is not a regular file, a language tag that names no language)
check Measures one or more drafts against --profile FOLDER and runs the copy guard. --lang (default: from the draft's script), --json, --strict, --accepted BASE for a revision of one unit. 0 measured · 1 the copy guard found a run, or with --strict a measure is outside and not weak, or could not be compared · 2 error, including a profile or guard that is not valid
rank Orders two or more takes of one unit, each checked alone: copy-guard runs last, then narration distance, voice distance and measures outside. --profile, --lang, --json. 0 ranking printed (it informs; it does not gate) · 2 error
book Checks every unit a --list file names, in reading order, one row each: mdrestyler book BOOK --profile FOLDER --list UNITS. --within sets the threshold: p50, p90 (default), max or a number. --lang, --json. 0 table printed · 2 a unit outside the book, missing, named twice or not Markdown, or an invalid profile
install Installs a profile into a profile folder: mdrestyler install FOLDER [SOURCE] --lang LANG. Without a source, the shipped default for the language. --replace replaces a draft or approved profile and keeps its files under history/. 0 installed (prints JSON) · 1 not installed, for example a profile already there and no --replace · 2 usage error
profile validate Reports a profile folder's status and revision as JSON, or why it is not valid: mdrestyler profile validate FOLDER --lang LANG. 0 valid · 2 not valid, or --lang missing
verify Checks the skill folder it runs from against MANIFEST.json; --expect DIGEST checks the manifest's digest too. Prints JSON with ok and manifest_sha256. 0 matches · 1 a file missing, changed or unlisted, or another digest · 2 not run from a skill folder
--version Prints the version, the source commit and the manifest digest, which is what a product built on mdrestyler pins. 0

Examples

mdrestyler scan --own mine.epub --reference theirs.epub --out profile --work work
mdrestyler scan --own-book my-book --eligibility eligibility.json --out profile --work work
mdrestyler install my-book/voice profile --lang en
mdrestyler profile validate my-book/voice --lang en
mdrestyler check --profile my-book/voice chapter-01.md --strict
mdrestyler check --profile my-book/voice revised.md --accepted base.md --json
mdrestyler rank --profile my-book/voice take-1.md take-2.md take-3.md
mdrestyler book my-book --profile my-book/voice --list units.txt --within p90
mdrestyler --version

The --list file names the units in reading order, one path per line relative to the book; blank lines and # lines are skipped. A unit with no prose, such as a part title, is listed as not measured.

Profiles and the copy guard

Reference books are measured, never quoted

A profile keeps measurements and described features of reference books, never their text. Only the author's own books may be quoted in it, and briefly. A text never names a reference author or says it imitates one.

A profile folder holds one profile per language
File Holds
AUTHOR_PROFILE.<lang>.md The described voice and a table of the measured targets; its status: default, draft or approved.
author-profile.<lang>.json The measures, target ranges, function-word fingerprint, calibration, and narration and dialogue ranges, with each source's hash. Schema mdrestyler-author-profile/1.
copy-guard.<lang>.json Winnowed hashes of the scanned books' 8-word runs, no text. Schema mdrestyler-copy-guard/1. The measures record its SHA-256 (copy_guard_sha256), so a guard from other books, or an edited one, is refused.
history/<time>/ Earlier profiles, kept when one is replaced.

Build, describe, approve, install

  1. Collect DRM-free EPUBs: the author's own (--own), books by other writers the author chooses (--reference), or both. An encrypted book is refused; never remove DRM.
  2. Scan into a profile folder, with the samples in a separate work folder, both outside any manuscript.
  3. Write the described voice in AUTHOR_PROFILE.<lang>.md: what the voice does, not only what it avoids. Keep observation, inference and the author's wishes apart.
  4. Delete the samples file. Show the profile to the author and record the corrections.
  5. When the author approves it, write the date on the one approval field under ## Approval, for example - Approved by the author on: 2026-10-03.
  6. Install it. A draft can be installed before approval and stays a draft.

mdrestyler ships a default for English and for Russian: a starting voice for contemporary literary prose, built from no book. An edited default reads as a draft.

The author's own Markdown book

Once the author confirms which units and passages are their original prose, write that confirmation as an eligibility file and scan with --own-book BOOK --eligibility FILE. Only eligible units are measured and sampled; every unit in order, notes included, feeds the copy guard. A unit whose bytes changed since its confirmation stops the scan.

{"decision": "Author's confirmation, 2026-10-01",
 "language": "en",
 "title": "The book's title",
 "order": ["01-first.md", "02-second.md"],
 "units": {
   "01-first.md": {"eligible": true, "sha256": "...",
                   "exclude": ["a passage that is not theirs"]},
   "02-second.md": {"eligible": false}}}

The profile's files are identified by their bytes. Keep Git from converting their line endings with a .gitattributes line such as voice/** -text.

The copy guard keeps the smallest hash in every eight consecutive 8-word runs of each book. It checks every run of a draft's words three ways: as a reader sees them, with markup removed but hidden words kept, and as raw text. It guards against copying, not concealment, and it is not proof of originality. A profile in a public repository is public, source list included.

For AI agents

An agent skill, JSON output and machine-readable docs

mdrestyler ships as an agent skill: SKILL.md and four references an AI coding agent reads before it builds a profile, drafts toward one or restyles a book. The rules travel with it: the measures locate, the reading decides, and a copy-guard hit blocks a candidate.

JSON output --json

check, rank and book print JSON with --json. A check holds copy_guard_hits, measures (each with value, low, high and status), voice_distance, registers for narration and dialogue, and the profile revision and status. scan, install, profile validate and verify always print JSON.

Building on it embed

A tool that embeds mdrestyler configures it once per process with mdrestyler.names.configure(…): its message names, who decides a delegated approval, its book layout, and which earlier profile schemas and defaults it still reads. Pin a release by its manifest digest from mdrestyler --version.

A prompt for a coding agent

Read https://sagol.github.io/mdrestyler/llms-full.txt and
https://raw.githubusercontent.com/sagol/mdrestyler/main/skills/mdrestyler/SKILL.md
with every file in its references/ folder. Then table the book:
mdrestyler book my-book --profile my-book/voice --list units.txt --within p90 --json
Assess every unit by reading before writing any take. Never choose a take,
or accept a change, by the numbers alone, and never splice takes.
/llms.txt A short llmstxt.org index: install, commands and links.
/llms-full.txt One file with every command and flag, exit codes, profile files and schemas, the restyle procedure, the measures and the limits.
/agents.json A JSON capability manifest, also pointed to from /.well-known/agent.json.
mdrestyler check --json The full report as JSON, for an agent to read field by field.
Limits

What a measurement does not tell you

A successful exit records a measurement. It does not establish quality, complete segmentation or originality.

  • Quality. The measures locate drift for a reader. They do not establish literary quality. Drift that the scene's purpose explains is a question for the author, not a fix.
  • Choosing takes. In one test the takes lay within 0.09 of each other, and the take the measure put first was the one a blind reader put last.
  • Originality. The copy guard guards against copying, not concealment. A shared run shorter than fifteen words is found only when one of its hashes was kept.
  • Short texts. Under 1,200 words the voice distance reads less reliably, and measures a draft is too short for are marked weak. Calibration needs five source chapters of 1,200 words or more.
  • Languages. English and Russian have word lists; other languages get only the language-neutral measures. There is no word segmenter for Han, kana, Thai, Lao, Khmer or Myanmar, so segmentation there is reported as partial and a guard with no hits says partial, not clear.
  • Russian dialogue. Dash-led speech is read as dialogue. Speech in «ёлочки» is measured as narration, because Russian prose sets titles, thoughts and quotations in them as often as speech.
  • Scan size. At most 64 MiB of expanded EPUB text across the books by default (--max-expanded-mb), with fixed ceilings of 256 MiB per EPUB, 10,000 entries, 16 MiB per text entry and 4,000,000 words.
  • Stylesheets. What an EPUB shows is judged from its CSS. Rules the scan cannot settle are never applied, and the profile marks its styles partial.
  • Reproducibility. On Python 3.10 and 3.11 a few full-precision values in --json output can differ in their last digit between runs; Python 3.12 and later, or PYTHONHASHSEED=0, give the same bytes every run.
  • Tests. The skill tests are static checks of the text: they say a rule is written, not that an agent follows it. Hosted CI may not run for the repository; local runs on Python 3.10 and 3.14 are the record. See VALIDATION.md.
Questions

Frequently asked questions

Does mdrestyler judge whether my prose is good?

No. The measures locate where a text drifts from a voice; a reading decides. A successful exit records a measurement. It does not establish literary quality, complete segmentation or originality, and mdrestyler never picks a take or accepts a change by the number.

Does a voice profile contain text from the books I scan?

Not from reference books. A profile keeps measurements and described features of reference books, never their text, and only the author's own books may be quoted in it. The copy guard file holds winnowed hashes of 8-word runs and no text. The scan writes sample passages to a separate work folder, which you delete once the profile is written.

Which books can I build a profile from?

DRM-free EPUBs: the author's own books (--own), books by other writers the author chooses (--reference), or both. An encrypted book is refused; never remove DRM. An author's own Markdown book can be scanned with --own-book and an eligibility file that records which units and passages are the author's original prose.

Which number should I watch during a restyle?

The narration distance. The pooled voice distance also weighs how much of a unit is dialogue, which follows composition and length more than voice. In one test a restyle moved the narration distance from 1.21 to 0.93 while the pooled distance went from 1.15 to 1.14.

Can mdrestyler choose the best of several takes?

No. The rank command orders takes: takes with a copy-guard run last, then by narration distance, voice distance and the number of measures outside. The order informs a reading and chooses nothing. In one test the take the measure put first was the one a blind reader put last.

What does the copy guard catch?

Any passage of fifteen space-delimited words or more that a draft shares with a scanned book, when it runs unbroken in one of three readings: as a reader sees the text, with markup removed but hidden words kept, or as raw text. A shorter shared run is found only when one of its hashes was kept. A hit exits 1 and blocks the draft. The guard is against copying, not concealment, and it is not proof of originality.

Which languages does mdrestyler support?

English and Russian have their own word lists. Other languages get the language-neutral measures. There is no word segmenter for Han, kana, Thai, Lao, Khmer or Myanmar; text in those scripts is processed, but segmentation is reported as partial.

What do I need to run mdrestyler?

Python 3.10 or later. mdrestyler uses the standard library only and has no dependencies. Install it with pip install mdrestyler, or copy the skills/mdrestyler/ folder and run python3 scripts/mdrestyler.py from inside it with nothing installed.

Can I run mdrestyler in CI?

Yes. The GitHub Action sagol/mdrestyler@v0.1.1 runs any mdrestyler command, set by the inputs command (default check), args and working-directory. The step exits with the command's status, so a copy-guard hit fails the job, and with --strict so does a measure outside its range.

Documentation