
Prose Style
- 476 installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
prose-style is an agent skill that shapes consistent voice, sentence rhythm, and diction for blogs, docs, newsletters, or in-app copy so drafts match a deliberate style guide instead of drifting generic.
About
prose-style is a jwynia/agent-skills writing governance skill for keeping generated prose aligned to a defined voice. It guides sentence rhythm, word choice, and tonal consistency across blogs, technical documentation, newsletters, and in-app copy so agent drafts do not read like generic LLM output. Developers and technical writers reach for prose-style when content ships alongside software—release notes, developer blogs, product copy, or long-form docs—and the team needs repeatable stylistic constraints rather than one-off rewrites. The skill acts as an editorial layer agents apply while drafting or revising text artifacts.
- Defines tone, register, and sentence-length targets
- Flags passive voice, clichés, and jargon drift
- Aligns microcopy with brand personality
- Offers before/after rewrites with rationale
- Scales style rules across multi-author teams
Prose Style by the numbers
- 476 all-time installs (skills.sh)
- +9 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #391 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill prose-styleAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 476 |
|---|---|
| repo stars | ★ 133 |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
How do you keep agent-written copy on brand?
Shape consistent voice, sentence rhythm, and diction for blogs, docs, newsletters, or in-app copy so drafts match a deliberate style guide instead of drifting generic.
Who is it for?
Developers and technical writers producing recurring content who need agent drafts to follow a deliberate style guide.
Skip if: Teams needing only code refactoring or API design help with no written content deliverables.
When should I use this skill?
Drafting or revising blogs, docs, newsletters, or product copy where voice consistency matters.
What you get
Style-aligned drafts for blogs, docs, newsletters, and in-app copy matching a defined prose guide.
- Style-aligned blog drafts
- Edited documentation copy
Files
Prose Style: Diagnostic Skill
You diagnose sentence-level craft problems in fiction. Your role is to identify why prose fails to serve the story and guide writers toward vigorous, intentional writing.
Core Principle
Style is not decoration; style is content.
The way something is written shapes what it means. As Truman Capote observed:
"I believe a story can be wrecked by a faulty rhythm in a sentence—especially if it occurs toward the end—or a mistake in paragraphing, even punctuation."
Prose style operates at multiple levels simultaneously:
- Word choice (diction)
- Sentence structure (syntax)
- Paragraph flow (rhythm)
- Voice (the writer's distinctive presence)
The fundamental principle: Vigorous writing is concise. Every word earns its place.
---
The Prose States
State P1: Flat Prose
Symptoms: Prose is functional but unmemorable. Sentences deliver information but have no rhythm or distinction. Writing doesn't enhance the story—it merely delivers it.
Key Questions:
- Is there sentence variety (length, structure)?
- Are word choices precise or generic?
- Is there any rhythm or is it monotonous?
- Does the prose have any distinctive quality?
Diagnostic Checklist:
- [ ] Sentences vary in length (short, medium, long)
- [ ] Sentences vary in structure (simple, compound, complex)
- [ ] Word choices are specific, not generic
- [ ] Prose has identifiable rhythm
Interventions:
- Read aloud to hear the rhythm (or lack thereof)
- Mark sentence lengths—look for variation
- Replace vague words with specific ones
- Vary sentence openings (don't always start with subject-verb)
---
State P2: Unclear Writing
Symptoms: Reader has to reread sentences. Meaning is obscured by abstraction or missing context. Pronouns lack clear antecedents. The curse of knowledge operates.
Key Questions:
- Are there too many abstractions?
- Is assumed knowledge preventing clarity?
- Are pronoun antecedents clear?
- Is logic visible or compressed?
Diagnostic Checklist:
- [ ] Concrete language outweighs abstract
- [ ] Context provided for specialized terms
- [ ] Every pronoun has obvious referent
- [ ] Logical steps are visible, not compressed
Interventions:
- Substitute concrete for abstract
- Add context where curse of knowledge operates
- Check every pronoun has an obvious referent
- Expand compressed thinking—show the steps
---
State P3: Overwrought Prose (Purple Prose)
Symptoms: Style overwhelms substance. Excessive adjectives and adverbs. Metaphors obscure rather than illuminate. Writing calls attention to itself rather than the story.
Key Questions:
- Are there adjective/adverb stacks?
- Do metaphors illuminate or obscure?
- Is style calling attention to itself?
- Does richness serve the work or overwhelm it?
Diagnostic Checklist:
- [ ] No more than 1-2 adjectives per noun
- [ ] Adverbs used sparingly, intentionally
- [ ] Metaphors clarify rather than confuse
- [ ] Prose serves story, not writer's ego
Signs of Purple Prose:
- Adjective stacking: "beautiful, gorgeous, stunning sunset"
- Adverb abuse: "ran quickly, desperately, frantically"
- Overwrought metaphor: comparisons that obscure
- Mismatched register: elevated language for mundane content
Interventions:
- Cut modifiers ruthlessly
- Choose one right adjective, not three approximations
- Replace overwrought metaphors with simpler images
- Let nouns and verbs do the work
---
State P4: Monotonous Prose
Symptoms: Every sentence sounds the same. Every paragraph looks the same. Sentences start the same way. Reading feels like a drone.
Key Questions:
- Are sentences all similar lengths?
- Are paragraphs all similar lengths?
- Do sentences start the same way?
- Is there any variation in rhythm?
Diagnostic Checklist:
- [ ] Sentence lengths vary significantly
- [ ] Paragraph lengths vary (including singles for punch)
- [ ] Sentence openings vary (not all subject-verb)
- [ ] Rhythm shifts between sections
Interventions:
- Consciously vary sentence length
- Use short sentences for punch, long for flow
- Vary paragraph length for rhythm
- Change sentence structure (simple, compound, complex)
- Vary sentence openings (modifiers, dependent clauses)
---
State P5: Passive Voice Overuse
Symptoms: Prose feels indirect, weak. Agents are routinely hidden when they matter. Energy drains from sentences. Action feels distant.
Key Questions:
- Are agents hidden when they matter?
- Does prose feel indirect?
- Is passive used intentionally or by default?
- Would active voice add energy?
When Passive IS Appropriate:
- Agent is unimportant ("The building was constructed in 1890")
- Agent is unknown ("Mistakes were made")
- Deliberately hiding the actor
- Emphasis at sentence end ("The patient was murdered by his own doctor!")
- Focus is on receiver ("Kennedy was assassinated")
Diagnostic Checklist:
- [ ] Passive voice used intentionally, not by default
- [ ] Important agents named, not hidden
- [ ] Active voice predominates in action sequences
- [ ] Passive serves emphasis where used
Interventions:
- Default to active voice
- Check each passive: is it intentional?
- If passive, does it serve emphasis, mystery, or receiver-focus?
- Convert default passives to active
---
State P6: Inconsistent Voice
Symptoms: Diction level shifts randomly. Sentence structure varies wildly without purpose. Different sections feel like different writers. Narrator doesn't sound like one person.
Key Questions:
- Does diction level shift without purpose?
- Does sentence structure vary wildly?
- Do different sections feel consistent?
- Is there a baseline voice to return to?
Levels of Diction:
| Level | Description | Example |
|---|---|---|
| High/Formal | Elevated, literary | "The conflagration consumed the edifice" |
| Middle/Standard | Educated but accessible | "The fire destroyed the building" |
| Low/Informal | Conversational | "The place burned down" |
Diagnostic Checklist:
- [ ] Diction level consistent for narrator
- [ ] Shifts in voice are intentional, not accidental
- [ ] Tone consistent across manuscript
- [ ] Relationship to reader (distance/intimacy) maintained
Interventions:
- Establish baseline voice (diction level, rhythm patterns)
- Vary from baseline intentionally for effect
- Ensure shifts are character/scene-driven, not author inconsistency
- Audit for intrusive author voice in close POV
---
The Strunk & White Principles
From The Elements of Style, foundational guidance:
1. Use the active voice (generally) 2. Put statements in positive form 3. Use definite, specific, concrete language 4. Omit needless words 5. Avoid a succession of loose sentences 6. Express coordinate ideas in similar form (parallelism) 7. Keep related words together 8. Place the emphatic words at the end
Caveat: These are principles, not laws. The goal is intentional choice, not mechanical obedience.
---
Word Choice Reference
Concrete vs. Abstract
Abstract: happiness, freedom, love, time Concrete: laughter, unlocked door, kiss, clock
The issue isn't abstraction itself—it's vague abstraction that avoids precision.
- Weak: "happiness"
- Strong: "the particular happiness of a child with a new dog"
Common Word Choice Traps
| Trap | Description | Fix |
|---|---|---|
| Thesaurus abuse | Obscure synonyms for common words | Use the right word, even if repeated |
| Elegant variation | Different words for same thing | Repetition is fine; clarity matters |
| Jargon creep | Technical language where plain works | Use simplest word that fits |
---
Sentence Structure Reference
The Punch Position
The end of a sentence carries the most weight.
- Weak: "It was a dark night, I remember"
- Strong: "I remember: it was a dark night"
Parallelism
Parallel structure creates rhythm and emphasis:
- "Veni, vidi, vici"
- "It was the best of times, it was the worst of times"
Faulty parallelism:
- Wrong: "She likes reading, to swim, and runs"
- Right: "She likes reading, swimming, and running"
Sentence Variety Guide
| Length | Effect | Use For |
|---|---|---|
| Short | Punch, urgency | Emphasis, action, revelation |
| Medium | Clarity, flow | Default narrative |
| Long | Development, immersion | Building complexity, flowing prose |
---
The Read-Aloud Test
The most reliable prose diagnostic: read it aloud.
What the ear catches that the eye misses:
- Awkward rhythm
- Repeated words
- Sentences that don't breathe
- Missing transitions
- Overwrought passages
"From the point of view of ear, Virginia Woolf never wrote a bad sentence."
— Truman Capote
Rule: If you stumble reading it, revise it.
---
Anti-Patterns
The Thesaurus Abuser
Pattern: Replacing common words with obscure synonyms for variety. Problem: Sacrifices clarity for artificial variety. Fix: Use the right word, even if you used it recently.
The Adjective Hoarder
Pattern: Stacking modifiers hoping something sticks. Problem: Weakens rather than strengthens description. Fix: Choose one right adjective. Or none—let the noun work.
The Passive Defaulter
Pattern: Writing in passive voice without intention. Problem: Prose loses energy and directness. Fix: Default to active. Use passive deliberately.
The Monotone
Pattern: Every sentence same length and structure. Problem: Creates droning effect; reader disengages. Fix: Vary intentionally. Short sentences punch. Long sentences flow.
The Purple Writer
Pattern: Style overwhelming substance. Problem: Reader sees the writing, not the story. Fix: Serve the story. Kill your darlings if they distract.
The Rule Slave
Pattern: Following every prescription mechanically. Problem: Loses the art in favor of rules. Fix: Understand principles, not just rules. Break rules intentionally.
---
Diagnostic Process
When a writer presents prose problems:
1. Identify the Problem Type
- Does it feel flat/boring? → P1 (Flat Prose)
- Is it hard to follow? → P2 (Unclear Writing)
- Does it feel overwritten? → P3 (Overwrought)
- Does everything sound the same? → P4 (Monotonous)
- Does it feel weak/indirect? → P5 (Passive Overuse)
- Does the voice shift randomly? → P6 (Inconsistent Voice)
2. Apply the Read-Aloud Test
Have writer read problematic passages aloud. What do they stumble on?
3. Check Multiple Levels
- Word level: precision, redundancy
- Sentence level: variety, clarity, parallelism
- Paragraph level: length, flow, transitions
- Voice level: consistency, diction, tone
4. Recommend Interventions
Based on identified state, provide specific fixes.
---
Integration with story-sense
| story-sense State | Maps to Prose Style State |
|---|---|
| State 5.9: Prose is Flat | P1-P6 (diagnose which specifically) |
When to Hand Off
- To revision: When prose issues require systematic pass through manuscript
- To dialogue: When prose problems appear specifically in dialogue
- To scene-sequencing: When rhythm problems are at scene level, not sentence level
Prerequisites
Do NOT use prose-style when:
- Structure is still broken (fix structure first)
- Scenes need cutting (don't polish what will be cut)
- Character arcs incomplete (fix story before prose)
Prose style is last-mile work. Complete developmental revision first.
---
Available Tools
prose-check.ts
Analyzes prose patterns for common issues.
deno run --allow-read scripts/prose-check.ts chapter.txt
deno run --allow-read scripts/prose-check.ts --text "The passive sentence was written..."Detects:
- Passive voice percentage
- Weak verb frequency
- Adverb density
- Filter word usage
- Adjective stacking
rhythm.ts
Analyzes rhythm and variety patterns.
deno run --allow-read scripts/rhythm.ts chapter.txt
deno run --allow-read scripts/rhythm.ts --text "Short. Then longer. Then short again."Reports:
- Sentence length distribution
- Paragraph length variation
- Opening word variety
- Rhythm score (variety metric)
---
Example Interactions
Example 1: Flat Prose
Writer: "My beta readers say my prose is functional but forgettable."
Your approach: 1. Identify state: P1 (Flat Prose) 2. Run rhythm.ts to check variety 3. Ask: "Read a paragraph aloud. What do you notice?" 4. Check: sentence lengths, word precision, rhythm 5. Recommend: vary sentence length, replace generic words with specific
Example 2: Purple Prose
Writer: "People say my writing is overwrought but I like rich prose."
Your approach: 1. Identify state: P3 (Overwrought) 2. Distinguish: Rich prose serves the story; purple overwhelms it 3. Ask: "Does the style serve the story or call attention to itself?" 4. Check for: adjective stacking, adverb abuse, mixed metaphors 5. Recommend: cut modifiers, simplify metaphors, let strong nouns/verbs work
Example 3: Inconsistent Voice
Writer: "Different chapters feel like different writers."
Your approach: 1. Identify state: P6 (Inconsistent Voice) 2. Ask: "What's your narrator's baseline voice?" 3. Check: diction level shifts, rhythm pattern changes 4. Recommend: establish baseline, vary intentionally from it
---
Output Persistence
This skill writes primary output to files so work persists across sessions.
Output Discovery
Before doing any other work:
1. Check for context/output-config.md in the project 2. If found, look for this skill's entry 3. If not found or no entry for this skill, ask the user first:
- "Where should I save output from this prose-style session?"
- Suggest:
explorations/prose/or a sensible location for this project
4. Store the user's preference:
- In
context/output-config.mdif context network exists - In
.prose-style-output.mdat project root otherwise
Primary Output
For this skill, persist:
- Prose state diagnosis - which style issues apply
- Sentence-level patterns - identified strengths and weaknesses
- Voice baseline notes - established voice characteristics
- Intervention recommendations - specific techniques to try
Conversation vs. File
| Goes to File | Stays in Conversation |
|---|---|
| Prose state diagnosis | Clarifying questions |
| Pattern identification | Discussion of specific passages |
| Voice baseline definition | Writer's experimentation |
| Recommended techniques | Real-time feedback |
File Naming
Pattern: {story}-prose-{date}.md Example: novel-chapter5-prose-2025-01-15.md
What You Do NOT Do
- You do not rewrite prose for writers
- You do not diagnose before structure is solid (hand off to story-sense)
- You do not make mechanical rules absolute
- You do not dismiss rich prose as automatically "purple"
Your role is diagnostic: identify the problem, explain why it's a problem, and guide toward the fix. The writer does the writing.
---
Key Insight
Prose is invisible when it works. The reader should experience the story, not notice the writing. When prose calls attention to itself—whether through flatness, confusion, or excess—it interrupts the dream.
The goal is not "good writing" in the abstract. The goal is writing that serves this specific story, these specific characters, this specific moment. Sometimes that means sparse. Sometimes rich. Always intentional.
#!/usr/bin/env -S deno run --allow-read
/**
* prose-check.ts - Analyze prose for common issues
*
* Detects:
* - Passive voice percentage
* - Weak verb frequency
* - Adverb density
* - Filter word usage
* - Adjective stacking
*
* Usage:
* deno run --allow-read prose-check.ts <file>
* deno run --allow-read prose-check.ts --text "Text to analyze..."
*/
// Passive voice indicators
const PASSIVE_PATTERNS = [
/\b(is|are|was|were|be|been|being)\s+(\w+ed|written|done|made|taken|given|seen|shown|known|found)\b/gi,
/\b(has|have|had)\s+been\s+\w+/gi,
/\bget(s|ting)?\s+\w+ed\b/gi,
];
// Weak verbs to flag
const WEAK_VERBS = [
'is', 'are', 'was', 'were', 'be', 'been', 'being',
'has', 'have', 'had',
'do', 'does', 'did',
'get', 'gets', 'got', 'gotten',
'make', 'makes', 'made',
'seem', 'seems', 'seemed',
'appear', 'appears', 'appeared',
'become', 'becomes', 'became',
];
// Filter words (distance readers from experience)
const FILTER_WORDS = [
'saw', 'see', 'sees', 'seen',
'heard', 'hear', 'hears',
'felt', 'feel', 'feels',
'noticed', 'notice', 'notices',
'realized', 'realize', 'realizes',
'thought', 'think', 'thinks',
'knew', 'know', 'knows',
'watched', 'watch', 'watches',
'wondered', 'wonder', 'wonders',
'decided', 'decide', 'decides',
];
// Common adverb endings and specific adverbs to flag
const ADVERB_PATTERN = /\b\w+ly\b/gi;
const DIALOGUE_ADVERBS = [
'quickly', 'slowly', 'softly', 'loudly', 'quietly',
'angrily', 'sadly', 'happily', 'nervously', 'anxiously',
'suddenly', 'immediately', 'finally', 'really', 'very',
'actually', 'basically', 'literally', 'definitely', 'certainly',
];
interface AnalysisResult {
totalSentences: number;
totalWords: number;
passiveCount: number;
passivePercentage: number;
passiveExamples: string[];
weakVerbCount: number;
weakVerbPercentage: number;
adverbCount: number;
adverbDensity: number;
adverbExamples: string[];
filterWordCount: number;
filterExamples: string[];
adjectiveStackCount: number;
stackExamples: string[];
}
function splitIntoSentences(text: string): string[] {
// Simple sentence splitter
return text.split(/[.!?]+/).filter(s => s.trim().length > 0);
}
function countWords(text: string): number {
return text.split(/\s+/).filter(w => w.length > 0).length;
}
function findPassiveVoice(text: string): { count: number; examples: string[] } {
const examples: string[] = [];
let count = 0;
const sentences = splitIntoSentences(text);
for (const sentence of sentences) {
for (const pattern of PASSIVE_PATTERNS) {
pattern.lastIndex = 0;
const matches = sentence.match(pattern);
if (matches) {
count += matches.length;
if (examples.length < 5) {
examples.push(sentence.trim().slice(0, 80) + (sentence.length > 80 ? '...' : ''));
}
}
}
}
return { count, examples: [...new Set(examples)] };
}
function countWeakVerbs(text: string): number {
const words = text.toLowerCase().split(/\s+/);
return words.filter(w => WEAK_VERBS.includes(w.replace(/[^a-z]/g, ''))).length;
}
function findAdverbs(text: string): { count: number; examples: string[] } {
const matches = text.match(ADVERB_PATTERN) || [];
const examples = matches
.filter(m => DIALOGUE_ADVERBS.includes(m.toLowerCase()))
.slice(0, 10);
return { count: matches.length, examples };
}
function findFilterWords(text: string): { count: number; examples: string[] } {
const words = text.toLowerCase().split(/\s+/);
const found = words.filter(w => FILTER_WORDS.includes(w.replace(/[^a-z]/g, '')));
return {
count: found.length,
examples: [...new Set(found)].slice(0, 10)
};
}
function findAdjectiveStacks(text: string): { count: number; examples: string[] } {
// Pattern: 2+ adjectives before a noun (simplified)
const pattern = /\b((\w+,?\s+){2,})(man|woman|person|thing|place|room|house|building|sky|day|night|face|eyes|voice|hand|way|time|door|wall|street|car|sun|moon|tree|water)\b/gi;
const matches = text.match(pattern) || [];
// Also check for comma-separated adjective lists
const commaPattern = /\b\w+,\s+\w+,\s+\w+\s+(and\s+)?\w+\b/gi;
const commaMatches = text.match(commaPattern) || [];
const examples = [...matches, ...commaMatches].slice(0, 5);
return { count: matches.length + commaMatches.length, examples };
}
function analyzeText(text: string): AnalysisResult {
const sentences = splitIntoSentences(text);
const totalWords = countWords(text);
const passive = findPassiveVoice(text);
const weakVerbCount = countWeakVerbs(text);
const adverbs = findAdverbs(text);
const filters = findFilterWords(text);
const stacks = findAdjectiveStacks(text);
return {
totalSentences: sentences.length,
totalWords,
passiveCount: passive.count,
passivePercentage: sentences.length > 0 ? (passive.count / sentences.length) * 100 : 0,
passiveExamples: passive.examples,
weakVerbCount,
weakVerbPercentage: totalWords > 0 ? (weakVerbCount / totalWords) * 100 : 0,
adverbCount: adverbs.count,
adverbDensity: totalWords > 0 ? (adverbs.count / totalWords) * 100 : 0,
adverbExamples: adverbs.examples,
filterWordCount: filters.count,
filterExamples: filters.examples,
adjectiveStackCount: stacks.count,
stackExamples: stacks.examples,
};
}
function getVerdict(result: AnalysisResult): string {
const issues: string[] = [];
if (result.passivePercentage > 20) {
issues.push('High passive voice usage (>20%)');
}
if (result.weakVerbPercentage > 15) {
issues.push('High weak verb frequency (>15%)');
}
if (result.adverbDensity > 4) {
issues.push('High adverb density (>4%)');
}
if (result.filterWordCount > result.totalSentences * 0.1) {
issues.push('Frequent filter words');
}
if (result.adjectiveStackCount > 3) {
issues.push('Multiple adjective stacks detected');
}
if (issues.length === 0) {
return 'Prose patterns look healthy';
} else if (issues.length <= 2) {
return 'Minor issues: ' + issues.join('; ');
} else {
return 'Multiple issues detected: ' + issues.join('; ');
}
}
function formatReport(result: AnalysisResult): string {
let report = `
PROSE ANALYSIS
==============
Overview:
Sentences: ${result.totalSentences}
Words: ${result.totalWords}
PASSIVE VOICE
Count: ${result.passiveCount}
Percentage: ${result.passivePercentage.toFixed(1)}% of sentences
Target: <20%`;
if (result.passiveExamples.length > 0) {
report += `\n Examples:\n${result.passiveExamples.map(e => ' - "' + e + '"').join('\n')}`;
}
report += `
WEAK VERBS (is/was/had/got/made/seemed)
Count: ${result.weakVerbCount}
Percentage: ${result.weakVerbPercentage.toFixed(1)}% of words
Target: <15%
ADVERBS
Count: ${result.adverbCount}
Density: ${result.adverbDensity.toFixed(1)}% of words
Target: <4%`;
if (result.adverbExamples.length > 0) {
report += `\n Flagged: ${result.adverbExamples.join(', ')}`;
}
report += `
FILTER WORDS (saw/felt/noticed/realized/etc.)
Count: ${result.filterWordCount}`;
if (result.filterExamples.length > 0) {
report += `\n Found: ${result.filterExamples.join(', ')}`;
}
report += `
ADJECTIVE STACKING
Count: ${result.adjectiveStackCount}`;
if (result.stackExamples.length > 0) {
report += `\n Examples:\n${result.stackExamples.map(e => ' - "' + e + '"').join('\n')}`;
}
report += `
VERDICT
${getVerdict(result)}
RECOMMENDATIONS:
- Passive voice: Use intentionally for emphasis or unknown agents
- Weak verbs: Replace with specific, active verbs where possible
- Adverbs: Cut or replace with stronger verbs
- Filter words: Remove to bring reader closer to experience
- Adjective stacks: Choose one precise adjective over many vague ones
`;
return report;
}
async function main() {
const args = Deno.args;
let text: string;
if (args.includes('--text')) {
const textIndex = args.indexOf('--text');
text = args.slice(textIndex + 1).join(' ');
} else if (args.length > 0) {
const filename = args[0];
try {
text = await Deno.readTextFile(filename);
} catch (e) {
console.error(`Error reading file: ${filename}`);
console.error(e);
Deno.exit(1);
}
} else {
console.log(`
Usage:
deno run --allow-read prose-check.ts <file>
deno run --allow-read prose-check.ts --text "Text to analyze..."
Analyzes prose for:
- Passive voice percentage
- Weak verb frequency
- Adverb density
- Filter word usage
- Adjective stacking
`);
Deno.exit(0);
}
const result = analyzeText(text);
console.log(formatReport(result));
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* rhythm.ts - Analyze prose rhythm and variety
*
* Reports:
* - Sentence length distribution
* - Paragraph length variation
* - Opening word variety
* - Rhythm score (variety metric)
*
* Usage:
* deno run --allow-read rhythm.ts <file>
* deno run --allow-read rhythm.ts --text "Short. Then longer. Short again."
*/
interface RhythmAnalysis {
sentences: SentenceStats;
paragraphs: ParagraphStats;
openings: OpeningStats;
rhythmScore: number;
verdict: string;
}
interface SentenceStats {
total: number;
lengths: number[];
short: number; // < 10 words
medium: number; // 10-20 words
long: number; // 21-35 words
veryLong: number; // > 35 words
avgLength: number;
stdDev: number;
varietyScore: number;
}
interface ParagraphStats {
total: number;
lengths: number[]; // sentences per paragraph
short: number; // 1-2 sentences
medium: number; // 3-5 sentences
long: number; // 6+ sentences
avgLength: number;
varietyScore: number;
}
interface OpeningStats {
openings: Map<string, number>;
uniqueRatio: number;
topOpenings: [string, number][];
repetitionWarnings: string[];
}
function splitIntoParagraphs(text: string): string[] {
return text.split(/\n\s*\n/).filter(p => p.trim().length > 0);
}
function splitIntoSentences(text: string): string[] {
// Handle common abbreviations
const cleanText = text
.replace(/Mr\./g, 'Mr')
.replace(/Mrs\./g, 'Mrs')
.replace(/Dr\./g, 'Dr')
.replace(/Ms\./g, 'Ms')
.replace(/\.\.\./g, '…');
return cleanText.split(/[.!?]+/).filter(s => s.trim().length > 0);
}
function countWords(text: string): number {
return text.split(/\s+/).filter(w => w.length > 0).length;
}
function getOpeningWord(sentence: string): string {
const words = sentence.trim().split(/\s+/);
if (words.length === 0) return '';
return words[0].toLowerCase().replace(/[^a-z]/g, '');
}
function calculateStdDev(values: number[], mean: number): number {
if (values.length < 2) return 0;
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / values.length;
return Math.sqrt(avgSquaredDiff);
}
function analyzeSentences(sentences: string[]): SentenceStats {
const lengths = sentences.map(s => countWords(s));
let short = 0, medium = 0, long = 0, veryLong = 0;
for (const len of lengths) {
if (len < 10) short++;
else if (len <= 20) medium++;
else if (len <= 35) long++;
else veryLong++;
}
const avgLength = lengths.length > 0
? lengths.reduce((a, b) => a + b, 0) / lengths.length
: 0;
const stdDev = calculateStdDev(lengths, avgLength);
// Variety score: higher std dev = more variety
// Normalize to 0-100 where 10+ std dev = 100
const varietyScore = Math.min(100, (stdDev / 10) * 100);
return {
total: sentences.length,
lengths,
short,
medium,
long,
veryLong,
avgLength,
stdDev,
varietyScore,
};
}
function analyzeParagraphs(paragraphs: string[]): ParagraphStats {
const lengths = paragraphs.map(p => splitIntoSentences(p).length);
let short = 0, medium = 0, long = 0;
for (const len of lengths) {
if (len <= 2) short++;
else if (len <= 5) medium++;
else long++;
}
const avgLength = lengths.length > 0
? lengths.reduce((a, b) => a + b, 0) / lengths.length
: 0;
// Variety score based on distribution across categories
const total = paragraphs.length;
let varietyScore = 0;
if (total > 0) {
const hasShort = short > 0;
const hasMedium = medium > 0;
const hasLong = long > 0;
const categoryCount = [hasShort, hasMedium, hasLong].filter(Boolean).length;
varietyScore = (categoryCount / 3) * 100;
}
return {
total: paragraphs.length,
lengths,
short,
medium,
long,
avgLength,
varietyScore,
};
}
function analyzeOpenings(sentences: string[]): OpeningStats {
const openings = new Map<string, number>();
for (const sentence of sentences) {
const opening = getOpeningWord(sentence);
if (opening) {
openings.set(opening, (openings.get(opening) || 0) + 1);
}
}
// Sort by frequency
const sorted = [...openings.entries()].sort((a, b) => b[1] - a[1]);
const topOpenings = sorted.slice(0, 10);
// Calculate unique ratio
const uniqueRatio = sentences.length > 0
? openings.size / sentences.length
: 0;
// Flag repetition warnings (same opening 4+ times, or >15% of sentences)
const repetitionWarnings: string[] = [];
const threshold = Math.max(4, sentences.length * 0.15);
for (const [word, count] of sorted) {
if (count >= threshold) {
repetitionWarnings.push(`"${word}" starts ${count} sentences (${((count / sentences.length) * 100).toFixed(0)}%)`);
}
}
return {
openings,
uniqueRatio,
topOpenings,
repetitionWarnings,
};
}
function calculateRhythmScore(
sentences: SentenceStats,
paragraphs: ParagraphStats,
openings: OpeningStats
): number {
// Weighted average of variety metrics
const sentenceWeight = 0.4;
const paragraphWeight = 0.3;
const openingWeight = 0.3;
const openingScore = openings.uniqueRatio * 100;
return (
sentences.varietyScore * sentenceWeight +
paragraphs.varietyScore * paragraphWeight +
openingScore * openingWeight
);
}
function getVerdict(analysis: RhythmAnalysis): string {
const issues: string[] = [];
// Check sentence variety
if (analysis.sentences.varietyScore < 30) {
issues.push('Low sentence length variety (monotonous)');
}
// Check for all-same-length issue
if (analysis.sentences.short === 0 && analysis.sentences.total > 5) {
issues.push('No short punchy sentences');
}
if (analysis.sentences.long === 0 && analysis.sentences.veryLong === 0 && analysis.sentences.total > 5) {
issues.push('No long flowing sentences');
}
// Check paragraph variety
if (analysis.paragraphs.varietyScore < 50) {
issues.push('Limited paragraph length variety');
}
// Check openings
if (analysis.openings.repetitionWarnings.length > 0) {
issues.push('Repetitive sentence openings');
}
if (issues.length === 0) {
return 'Good rhythm variety';
} else if (issues.length <= 2) {
return 'Some rhythm issues: ' + issues.join('; ');
} else {
return 'Monotonous prose: ' + issues.join('; ');
}
}
function analyzeText(text: string): RhythmAnalysis {
const paragraphs = splitIntoParagraphs(text);
const sentences = splitIntoSentences(text);
const sentenceStats = analyzeSentences(sentences);
const paragraphStats = analyzeParagraphs(paragraphs);
const openingStats = analyzeOpenings(sentences);
const rhythmScore = calculateRhythmScore(sentenceStats, paragraphStats, openingStats);
const verdict = getVerdict({
sentences: sentenceStats,
paragraphs: paragraphStats,
openings: openingStats,
rhythmScore,
verdict: '', // placeholder
});
return {
sentences: sentenceStats,
paragraphs: paragraphStats,
openings: openingStats,
rhythmScore,
verdict,
};
}
function formatLengthHistogram(lengths: number[], bucketSize: number = 5): string {
const buckets = new Map<number, number>();
for (const len of lengths) {
const bucket = Math.floor(len / bucketSize) * bucketSize;
buckets.set(bucket, (buckets.get(bucket) || 0) + 1);
}
const sortedBuckets = [...buckets.entries()].sort((a, b) => a[0] - b[0]);
const maxCount = Math.max(...sortedBuckets.map(b => b[1]));
const scale = Math.min(30, maxCount);
let histogram = '';
for (const [bucket, count] of sortedBuckets) {
const barLength = Math.round((count / maxCount) * scale);
const bar = '█'.repeat(barLength);
histogram += ` ${String(bucket).padStart(2)}-${String(bucket + bucketSize - 1).padEnd(2)}: ${bar} (${count})\n`;
}
return histogram;
}
function formatReport(analysis: RhythmAnalysis): string {
const { sentences, paragraphs, openings, rhythmScore, verdict } = analysis;
let report = `
RHYTHM ANALYSIS
===============
OVERALL RHYTHM SCORE: ${rhythmScore.toFixed(0)}/100
${verdict}
SENTENCE LENGTH
Total sentences: ${sentences.total}
Average length: ${sentences.avgLength.toFixed(1)} words
Standard deviation: ${sentences.stdDev.toFixed(1)}
Variety score: ${sentences.varietyScore.toFixed(0)}/100
Distribution:
Short (<10 words): ${sentences.short} (${((sentences.short / sentences.total) * 100).toFixed(0)}%)
Medium (10-20 words): ${sentences.medium} (${((sentences.medium / sentences.total) * 100).toFixed(0)}%)
Long (21-35 words): ${sentences.long} (${((sentences.long / sentences.total) * 100).toFixed(0)}%)
Very long (>35): ${sentences.veryLong} (${((sentences.veryLong / sentences.total) * 100).toFixed(0)}%)
Histogram (words per sentence):
${formatLengthHistogram(sentences.lengths, 5)}
PARAGRAPH LENGTH
Total paragraphs: ${paragraphs.total}
Average length: ${paragraphs.avgLength.toFixed(1)} sentences
Variety score: ${paragraphs.varietyScore.toFixed(0)}/100
Distribution:
Short (1-2 sentences): ${paragraphs.short} (${paragraphs.total > 0 ? ((paragraphs.short / paragraphs.total) * 100).toFixed(0) : 0}%)
Medium (3-5 sentences): ${paragraphs.medium} (${paragraphs.total > 0 ? ((paragraphs.medium / paragraphs.total) * 100).toFixed(0) : 0}%)
Long (6+ sentences): ${paragraphs.long} (${paragraphs.total > 0 ? ((paragraphs.long / paragraphs.total) * 100).toFixed(0) : 0}%)
SENTENCE OPENINGS
Unique ratio: ${(openings.uniqueRatio * 100).toFixed(0)}% (${openings.openings.size} unique / ${sentences.total} sentences)
Most common openings:
${openings.topOpenings.map(([word, count]) => ` "${word}": ${count} times (${((count / sentences.total) * 100).toFixed(0)}%)`).join('\n')}
`;
if (openings.repetitionWarnings.length > 0) {
report += `
WARNINGS:
${openings.repetitionWarnings.map(w => ` ! ${w}`).join('\n')}
`;
}
report += `
RECOMMENDATIONS:
- If monotonous: vary sentence lengths consciously
- Short sentences punch, long sentences flow
- Use single-sentence paragraphs for emphasis
- Vary sentence openings (not all "The" or "He/She")
- Mix simple, compound, and complex sentences
`;
return report;
}
async function main() {
const args = Deno.args;
let text: string;
if (args.includes('--text')) {
const textIndex = args.indexOf('--text');
text = args.slice(textIndex + 1).join(' ');
} else if (args.length > 0) {
const filename = args[0];
try {
text = await Deno.readTextFile(filename);
} catch (e) {
console.error(`Error reading file: ${filename}`);
console.error(e);
Deno.exit(1);
}
} else {
console.log(`
Usage:
deno run --allow-read rhythm.ts <file>
deno run --allow-read rhythm.ts --text "Short. Then longer. Short again."
Analyzes prose rhythm for:
- Sentence length distribution
- Paragraph length variation
- Opening word variety
- Overall rhythm score
`);
Deno.exit(0);
}
const analysis = analyzeText(text);
console.log(formatReport(analysis));
}
main();
Related skills
FAQ
What content types does prose-style cover?
prose-style covers blogs, technical documentation, newsletters, and in-app copy, shaping voice, sentence rhythm, and diction to match a deliberate style guide.
Why use prose-style instead of generic rewriting?
prose-style applies repeatable stylistic constraints—voice, rhythm, and diction—so agent drafts stay on-brand rather than drifting into generic LLM phrasing.