Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/plugin-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ jobs:

- name: Validate plugin manifests and draw rotation
run: python3 tools/validate_plugin.py

- name: Run prose fingerprint tests
run: python3 -m unittest discover -s tests
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Built for longform authors, essayists, ghostwriters, and editors. Runs a team of

## The Problem

AI-generated writing converges on the same cadence. "Delve into the tapestry of." "It's important to note that." "Not just X, but Y." Em-dashes everywhere. Three-word lists. Conclusion paragraphs that restate the previous five. Hedging openers.
AI-generated writing converges on the same cadence. "Delve into the tapestry of." "It's important to note that." "Not just X, but Y." "This is not X. It is Y." Em-dashes everywhere. Three-word lists. Conclusion paragraphs that restate the previous five. Hedging openers.

Scribe fixes this with persona-first generation and a sameness score that quantifies how close prose is to the AI default.

Expand Down Expand Up @@ -95,7 +95,7 @@ Scribe ships with anti-patterns across three files:

| File | Patterns |
|---|---|
| `anti-patterns/language.md` | Delve/tapestry/realm vocabulary, tricolon reflex, hedge stacking, marketing verbs, em-dash overuse, self-descriptors, filler intensifiers |
| `anti-patterns/language.md` | Delve/tapestry/realm vocabulary, tricolon reflex, hedge stacking, marketing verbs, em-dash overuse, self-descriptors, negative/positive restatement, filler intensifiers |
| `anti-patterns/rhetoric.md` | Universal aphorisms, "not just X but Y" frames, throat-clearing openers, flashlight-hedging adjectives |
| `anti-patterns/structure.md` | Conclusion-restating, predictable section sequence, thesis-delay syndrome |

Expand Down
2 changes: 2 additions & 0 deletions agents/ai-ism-critic.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Catch every instance of the following:
- Em-dashes used as universal connectors (>4 per page in any persona)
- Tricolons (three-word adjective lists for emphasis)
- "Not just X, but Y" frames
- Negative/positive restatements: "This is not X. It is Y," "The goal is not X. The goal is Y," or stacked negation followed by positive restatement
- "It's not about X, it's about Y" binaries
- "And yet" twist sentences
- "What most people don't realize..." pseudo-reveals
Expand Down Expand Up @@ -74,6 +75,7 @@ Each fingerprint type has a cost:
| Em-dashes over budget | +1 each over |
| Tricolons | +2 each, capped at +6 |
| "Not just X, but Y" / binary frames | +3 each, capped at +6 |
| Negative/positive restatement | +3 each, capped at +9 |
| "And yet" twist (literal phrase) | +3 each |
| Universal aphorism | +5 each, capped at +10 |
| Concluding hortatory | +5 each |
Expand Down
4 changes: 2 additions & 2 deletions agents/anti-pattern-selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ Build `must_include` — patterns whose `personas` or `site_signals` strongly ma
|---|---|
| `surface` is `chapter` or `essay` | At least 2 from: `chapter-recap-opening`, `chapter-end-summary`, `forward-reference`, `predictable-section-sequence`, `conclusion-restating-conclusion` |
| `surface` is `opening` | `in-todays-fast-paced-world`, `delve-tapestry-realm`, `universalizing-we-many-people` |
| `surface` is `conclusion` | `concluding-hortatory`, `aphorism-section-closer`, `conclusion-restating-conclusion` |
| `persona` is `business-book` | At least 2 from: `marketing-verb`, `not-x-but-y-frame`, `three-anecdote-montage`, `chapter-end-summary`, `concluding-hortatory` |
| `surface` is `conclusion` | `concluding-hortatory`, `aphorism-section-closer`, `conclusion-restating-conclusion`, `negative-positive-restatement` |
| `persona` is `business-book` | At least 2 from: `marketing-verb`, `not-x-but-y-frame`, `negative-positive-restatement`, `three-anecdote-montage`, `chapter-end-summary`, `concluding-hortatory` |
| `persona` is `memoir` | `universal-aphorism`, `flashlight-hedging-adjectives`, `aphorism-section-closer` |
| `persona` is `technical-writing` | `marketing-verb`, `self-descriptor`, `bullet-list-body` |
| `persona` is `literary-fiction` | `tricolon-reflex`, `em-dash-universal-connector` |
Expand Down
1 change: 1 addition & 0 deletions agents/draft-implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ You are the first writer. You produce the first draft of the requested passage b
- Do not produce three-word lists (tricolons) unless the rhythm-spec explicitly allows them in this run.
- Do not produce paragraphs of uniform length. Hit the standard-deviation target.
- Do not include "key takeaways," "in conclusion," "as we have seen," or any other recap phrase from selected_subset.
- Do not produce negative/positive restatement: avoid "not X, but Y," "This is not X. It is Y," and "The goal is not X. The goal is Y" unless the contrast is specific and necessary.
- Do not write a meta-introduction explaining what you are about to do. Begin the passage cold.

## Output
Expand Down
1 change: 1 addition & 0 deletions agents/voice-implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Scan for any phrase from the persona's "Forbidden Openers" list — replace.
Scan for any vocabulary on the banned list (delve, tapestry, navigate-as-metaphor, etc.) — replace with concrete alternative.
Scan for "It's worth noting that," "It's important to," "In a world where," "many people," "we all," etc. — cut.
Scan for tricolons — replace with two-item or four-item lists.
Scan for negative/positive restatement ("This is not X. It is Y," "The goal is not X. The goal is Y"): state the positive claim directly unless the contrast is necessary.
Scan for "and yet" twists — restructure or cut the literal phrase.
Scan for marketing verbs (unlock, leverage, transform, robust, powerful) — replace.

Expand Down
29 changes: 28 additions & 1 deletion anti-patterns/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ file: anti-patterns/language.md
tags: [vocabulary, ai-isms, register, idiom, hedging]
affects: all
severity_default: high
pattern_count: 12
pattern_count: 13
---

# Language Anti-Patterns
Expand Down Expand Up @@ -179,6 +179,33 @@ _Tags: rhythm, rhetoric · Affects: all · Severity: high_

---

## Negative/Positive Restatement
_Tags: ai-isms, rhetoric, restatement · Affects: all · Severity: high_

**What it is**: A broader cousin of the "not just X" reflex. The sentence negates a weak version of the claim, then immediately states the preferred positive version. Common forms include "This is not X. It is Y," "This is not about X. It is about Y," "Not X. Y," and stacked variants that repeat the move across a paragraph.

**Why it fails**: The negated half is usually a straw frame the reader did not need. AI prose uses the pattern to create fake decisiveness. The result sounds polished but padded: first it says what the thing is not, then it says what it meant to say.

**Fix**: State the positive claim once. Keep the negative half only when the reader probably believes the false version and the contrast is specific.

```
// AI restatement
This is not a productivity trick. It is a new operating model for teams.

// Direct claim
It changes how teams decide what work counts as finished.
```

```
// AI restatement
The goal is not to replace judgment. The goal is to put judgment in the right place.

// Direct claim
Put judgment where it belongs: use the model for context-sensitive choices and use policy for boundaries that must not drift.
```

---

## Flashlight-Hedging Adjectives
_Tags: voice, register · Affects: all · Severity: moderate_

Expand Down
14 changes: 13 additions & 1 deletion anti-patterns/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"universalizing",
"false-dichotomy",
"tricolon",
"dialogue"
"dialogue",
"restatement"
],
"patterns": [
{
Expand Down Expand Up @@ -140,6 +141,17 @@
"personas": ["all"],
"site_signals": ["any"]
},
{
"id": "negative-positive-restatement",
"title": "Negative/Positive Restatement",
"categories": ["ai-isms", "rhetoric", "restatement"],
"tags": ["not-x-then-y", "padded-contrast", "ai-default"],
"severity_default": "high",
"source_file": "anti-patterns/language.md",
"anchor": "negativepositive-restatement",
"personas": ["all"],
"site_signals": ["any", "chapter", "essay", "conclusion"]
},
{
"id": "flashlight-hedging-adjectives",
"title": "Flashlight-Hedging Adjectives",
Expand Down
6 changes: 3 additions & 3 deletions skills/scribe/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: scribe
version: 1.0.0
description: Multi-agent writing harness for longform authors. Catches AI-default writing patterns (delve, tapestry, em-dash overuse, hedging openers, three-word lists, conclusion-restating) before they ship. Five writer personas (literary fiction, popular nonfiction, technical writing, memoir, business book), persona-specific rhythm and voice rules, anti-pattern rotation per run with audit log, multi-critic synthesis. Built for authors, essayists, ghostwriters, and editors.
description: Multi-agent writing harness for longform authors. Catches AI-default writing patterns (delve, tapestry, em-dash overuse, hedging openers, three-word lists, negative/positive restatement, conclusion-restating) before they ship. Five writer personas (literary fiction, popular nonfiction, technical writing, memoir, business book), persona-specific rhythm and voice rules, anti-pattern rotation per run with audit log, multi-critic synthesis. Built for authors, essayists, ghostwriters, and editors.
---

# Scribe v1.0.0
Expand Down Expand Up @@ -134,12 +134,12 @@ The pool is grouped into three base files plus a research-sourced directory:

| File | Patterns |
|---|---|
| `${CLAUDE_PLUGIN_ROOT}/anti-patterns/language.md` | Delve / tapestry, tricolons, hedge stacking, em-dash universal connector, marketing verb, self-descriptor, universal aphorism, "not just X but Y" frames, filler intensifiers |
| `${CLAUDE_PLUGIN_ROOT}/anti-patterns/language.md` | Delve / tapestry, tricolons, hedge stacking, em-dash universal connector, marketing verb, self-descriptor, universal aphorism, "not just X but Y" frames, negative/positive restatement, filler intensifiers |
| `${CLAUDE_PLUGIN_ROOT}/anti-patterns/structure.md` | Uniform paragraph length, chapter-recap opening, chapter-end summary, forward references, three-part skeleton, predictable section sequence, bullet-list body, three-anecdote montage, false-question subhead, conclusion-restating-conclusion |
| `${CLAUDE_PLUGIN_ROOT}/anti-patterns/rhetoric.md` | "In today's fast-paced world" opener, universalizing claims, false dichotomies, rhetorical question reflex, aphorism section closers, "what most don't realize" pseudo-reveal, "and yet" twist, concluding hortatory |
| `${CLAUDE_PLUGIN_ROOT}/anti-patterns/sourced-from-research/` | Filed by the research-scout agent. Each pattern requires 2+ independent complaints. |

Total at v0.1: 30 anti-patterns. Will grow through ralph-loop iterations and `/scribe:research` runs.
Total at v1.0: 31 anti-patterns. Will grow through ralph-loop iterations and `/scribe:research` runs.

## Sameness Score

Expand Down
40 changes: 40 additions & 0 deletions tests/test_prose_fingerprints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from pathlib import Path
import importlib.util
import sys
import unittest

ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = ROOT / "tools" / "prose_fingerprints.py"
SPEC = importlib.util.spec_from_file_location("prose_fingerprints", MODULE_PATH)
assert SPEC and SPEC.loader
prose_fingerprints = importlib.util.module_from_spec(SPEC)
sys.modules["prose_fingerprints"] = prose_fingerprints
SPEC.loader.exec_module(prose_fingerprints)
detect_negative_positive_restatement = prose_fingerprints.detect_negative_positive_restatement


class NegativePositiveRestatementTests(unittest.TestCase):
def test_detects_adjacent_sentence_restatement(self):
text = "This is not a productivity trick. It is a new operating model for teams."
findings = detect_negative_positive_restatement(text)
self.assertEqual(len(findings), 1)
self.assertEqual(findings[0].pattern_id, "negative-positive-restatement")
self.assertEqual(findings[0].kind, "not_x_it_is_y")

def test_detects_not_about_about_frame(self):
text = "The goal is not about replacing judgment. The goal is about putting judgment in the right place."
findings = detect_negative_positive_restatement(text)
self.assertTrue(any(item.kind == "not_about_about" for item in findings))

def test_detects_not_just_but_frame(self):
text = "It is not just a checklist, but a complete operating model."
findings = detect_negative_positive_restatement(text)
self.assertTrue(any(item.kind == "not_just_but" for item in findings))

def test_allows_specific_contrast_without_immediate_positive_restating(self):
text = "The job did not finish because the database was unavailable. The retry queue preserved the failed work item."
self.assertEqual(detect_negative_positive_restatement(text), [])


if __name__ == "__main__":
unittest.main()
7 changes: 4 additions & 3 deletions tools/draw_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"marketing-verb",
"em-dash-connector",
"self-descriptor",
"negative-positive-restatement",
}

CATEGORY_DIVERSITY_MIN = (
Expand Down Expand Up @@ -178,13 +179,13 @@ def take(*ids: str) -> list[str]:
if inputs.persona == "literary-fiction":
forced += take("tricolon-reflex", "em-dash-connector")
elif inputs.persona == "popular-nonfiction":
forced += take("hedge-stacking", "filler-openers", "universal-aphorism")
forced += take("hedge-stacking", "negative-positive-restatement", "universal-aphorism")
elif inputs.persona == "technical-writing":
forced += take("hedge-stacking", "marketing-verb", "filler-openers")
forced += take("hedge-stacking", "marketing-verb", "negative-positive-restatement")
elif inputs.persona == "memoir":
forced += take("tricolon-reflex", "self-descriptor", "universal-aphorism")
elif inputs.persona == "business-book":
forced += take("marketing-verb", "hedge-stacking", "filler-openers")
forced += take("marketing-verb", "hedge-stacking", "negative-positive-restatement")

seen: set[str] = set()
deduped: list[str] = []
Expand Down
106 changes: 106 additions & 0 deletions tools/prose_fingerprints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Lightweight prose fingerprint detectors for Scribe validation."""

from __future__ import annotations

import argparse
import json
import re
from dataclasses import asdict, dataclass
from pathlib import Path


@dataclass(frozen=True)
class Finding:
pattern_id: str
kind: str
line: int
excerpt: str


NEGATIVE_POSITIVE_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
(
"not_just_but",
re.compile(
r"\bnot\s+(?:just|only|merely)\b[^.!?;]{0,160}\bbut(?:\s+also)?\b[^.!?]{0,160}",
re.IGNORECASE,
),
),
(
"not_about_about",
re.compile(
r"\b(?:it|this|that|the\s+goal|the\s+point)\s+(?:is|'s)\s+not\s+about\b[^.!?]{0,140}"
r"[.!?;,]\s*(?:it|this|that|the\s+goal|the\s+point)\s+(?:is|'s)\s+about\b[^.!?]{0,140}",
re.IGNORECASE,
),
),
(
"not_x_it_is_y",
re.compile(
r"\b(?:it|this|that|the\s+goal|the\s+point|the\s+work|the\s+system)\s+(?:is|'s)\s+not\b[^.!?]{3,140}"
r"[.!?;]\s*(?:it|this|that|the\s+goal|the\s+point|the\s+work|the\s+system)\s+(?:is|'s)\b[^.!?]{3,140}",
re.IGNORECASE,
),
),
(
"not_fragment_then_claim",
re.compile(
r"(?:^|[.!?]\s+)Not\s+(?:a\s+|an\s+|the\s+)?[^.!?]{3,100}[.!?]\s+[A-Z][^.!?]{3,140}",
),
),
)


def line_for_offset(text: str, offset: int) -> int:
return text.count("\n", 0, offset) + 1


def compact_excerpt(value: str, limit: int = 220) -> str:
excerpt = re.sub(r"\s+", " ", value).strip()
if len(excerpt) <= limit:
return excerpt
return excerpt[: limit - 1].rstrip() + "..."


def detect_negative_positive_restatement(text: str) -> list[Finding]:
"""Find negated claim followed by immediate positive restatement."""
findings: list[Finding] = []
seen: set[tuple[int, int, str]] = set()

for kind, pattern in NEGATIVE_POSITIVE_PATTERNS:
for match in pattern.finditer(text):
key = (match.start(), match.end(), kind)
if key in seen:
continue
seen.add(key)
findings.append(
Finding(
pattern_id="negative-positive-restatement",
kind=kind,
line=line_for_offset(text, match.start()),
excerpt=compact_excerpt(match.group(0)),
)
)

findings.sort(key=lambda item: (item.line, item.excerpt))
return findings


def main() -> int:
parser = argparse.ArgumentParser(description="Detect Scribe AI-default prose fingerprints.")
parser.add_argument("path", help="Text or Markdown file to inspect")
parser.add_argument("--json", action="store_true", help="Emit machine-readable findings")
args = parser.parse_args()

text = Path(args.path).read_text(encoding="utf-8")
findings = detect_negative_positive_restatement(text)
if args.json:
print(json.dumps([asdict(item) for item in findings], indent=2))
else:
for item in findings:
print(f"{item.pattern_id}:{item.line}:{item.kind}: {item.excerpt}")
return 1 if findings else 0


if __name__ == "__main__":
raise SystemExit(main())
26 changes: 26 additions & 0 deletions tools/validate_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import json
import importlib.util
import re
import subprocess
import sys
Expand Down Expand Up @@ -187,12 +188,37 @@ def validate_draw_rotation() -> None:
fail("draw simulator subset size outside expected range")


def validate_negative_positive_detector() -> None:
module_path = ROOT / "tools" / "prose_fingerprints.py"
spec = importlib.util.spec_from_file_location("prose_fingerprints", module_path)
if spec is None or spec.loader is None:
fail("could not load prose_fingerprints detector")
module = importlib.util.module_from_spec(spec)
sys.modules["prose_fingerprints"] = module
spec.loader.exec_module(module)

positive_cases = [
"This is not a productivity trick. It is a new operating model for teams.",
"The goal is not about replacing judgment. The goal is about putting judgment in the right place.",
"It is not just a checklist, but a complete operating model.",
]
for case in positive_cases:
findings = module.detect_negative_positive_restatement(case)
if not findings:
fail(f"negative/positive detector missed: {case}")

negative_case = "The job did not finish because the database was unavailable. The retry queue preserved the failed work item."
if module.detect_negative_positive_restatement(negative_case):
fail("negative/positive detector flagged a specific non-restatement failure sentence")


def main() -> int:
validate_plugin_manifests()
validate_agents_and_commands()
validate_antipattern_manifest()
validate_personas()
validate_draw_rotation()
validate_negative_positive_detector()
print("Scribe plugin validation passed")
return 0

Expand Down
Loading