| name | law-document |
| description | Draft and produce formatted Word document (.docx) general documents - proposals, reports, briefing docs, white papers, and other formal written materials - in Penn Carey Law style. Use this skill whenever the user asks to write, draft, or create a document that is not a memo or email: proposals, policy documents, reports, briefing materials, white papers, or any substantive standalone written work. Trigger phrases include write a proposal, draft a document, create a report, briefing doc, policy document, white paper, or any request to produce a formal standalone document. Use the law-memo skill instead for memos specifically. |
| license | CC-BY-4.0 |
| compatibility | Requires python-docx |
| metadata | {"author":"[Your Name]"} |
Law Document Skill
Produces professional Penn Carey Law documents as .docx files — proposals, reports, briefing docs, and similar materials. Shares formatting conventions with the memo skill (Cambria, 1" margins) but uses document-appropriate structure rather than the memo header block.
Agent Dependencies
This skill dispatches sub-agents for pre-delivery quality checks. Each call is guarded — the document still produces without them, but factual and style verification are weaker.
factual-reviewer — extracts discrete factual claims for verification.
fact-verifier — live web/source verification of specific claims.
voice-style-checker — voice, style, and AI-tell scan.
Install from the agents/ directory of this skill's repo into ~/.claude/agents/.
Environment
This skill works in both Claude Code CLI and Claude.ai / Cowork. Use whichever paths exist:
- Skills:
~/.claude/skills/ (CLI) or /mnt/skills/user/ (web)
- Output:
~/Downloads/ or user-specified path (CLI) or /mnt/user-data/outputs/ (web)
Before Drafting
- Read the docx skill if available — all file production follows those instructions
- Identify the document type (see patterns below) and clarify with [Your Name] if needed
- Confirm: audience, purpose, approximate length
Logo — Required First Step
Every document begins with the Penn Carey Law logo centered in the title block. This is the first step of file production, not a checklist item.
Path: ~/.claude/skills/law-document/assets/PennCareyLaw_UPenn_Blue-WhiteBkrnd.png (CLI)
or /mnt/skills/user/law-document/assets/PennCareyLaw_UPenn_Blue-WhiteBkrnd.png (web).
Try CLI path first; if not found, try web path.
If neither path works, stop and tell the user — do not produce a document without it.
Sizing: The logo must be resized proportionally. The source image is 2000×358 pixels (aspect ratio 5.587:1). Target width is 2.875 inches.
- EMU values:
cx="2628900" cy="470573"
- Formula: 1 inch = 914400 EMU. Width = 2.875 × 914400 = 2628900. Height = width × (358 / 2000) = 470573.
- Never set width and height independently — always derive height from width using the source aspect ratio.
from docx.shared import Emu
for p in [
os.path.expanduser("~/.claude/skills/law-document/assets/PennCareyLaw_UPenn_Blue-WhiteBkrnd.png"),
"/mnt/skills/user/law-document/assets/PennCareyLaw_UPenn_Blue-WhiteBkrnd.png",
]:
if os.path.exists(p):
LOGO_PATH = p
break
else:
raise FileNotFoundError("Penn Carey Law logo not found at any known path")
LOGO_WIDTH = Emu(2628900)
LOGO_HEIGHT = Emu(470573)
logo_paragraph = document.add_paragraph()
logo_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
logo_paragraph.paragraph_format.space_after = Pt(10)
run = logo_paragraph.add_run()
run.add_picture(LOGO_PATH, width=LOGO_WIDTH, height=LOGO_HEIGHT)
Document Formatting Spec
All formatting uses the same precise values as the memo skill so documents from the same office look consistent.
Page Setup
- Margins: 1" on all sides (1440 twips)
- Font: Cambria throughout — headings AND body. No other fonts.
- Body size: 12pt (24 half-points)
- Line spacing:
w:line="276" w:lineRule="auto" (~1.15)
- Paragraph spacing:
w:after="160" for body text
from docx.shared import Pt, Inches, Twips
from docx.enum.text import WD_ALIGN_PARAGRAPH
section = document.sections[0]
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1)
section.right_margin = Inches(1)
Title Block (for proposals and reports)
[Penn Carey Law logo — centered, 2.875" wide, proportionally scaled]
[DOCUMENT TITLE] ← centered, bold, Cambria 12pt, all caps or title case
[Subtitle if needed] ← centered, not bold
[Author / Date / Prepared for — left aligned or centered, as appropriate]
──────────────────────────────────── ← horizontal rule (bottom border)
Title paragraph: alignment=CENTER, Cambria bold 12pt, w:after="80"
For shorter or internal documents, a simpler title is fine — just bold centered title, date below.
Horizontal Rule
Same as memo skill — paragraph with bottom border:
<w:pBdr><w:bottom w:val="single" w:color="000000" w:sz="6" w:space="1"/></w:pBdr>
With w:after="240".
Headings
- Section headings: Bold, left-aligned, Cambria 12pt — same size as body, weight distinguishes them.
w:before="200" w:after="80"
- Sub-headings: Bold italic, left-aligned, 12pt.
w:before="160" w:after="80"
- No heading numbering unless document is long (5+ sections) and navigation is useful
Body Text
- Cambria 12pt,
w:line="276" w:lineRule="auto" w:after="160"
- Paragraphs separated by spacing, not blank lines
- Bold used sparingly for key terms, action items, or critical facts
- Tables: clean, minimal borders; Cambria 12pt in cells; used for comparative or structured data; must not split across pages (see Tables section below)
Tables
Tables must not split across pages. After building any table, apply cantSplit to every row and keepNext to all paragraphs in every row except the last. This prevents individual rows from breaking mid-row and keeps the entire table on one page.
def prevent_table_split(table):
"""Prevent table rows from splitting across pages and keep table together."""
rows = table.rows
for i, row in enumerate(rows):
tr = row._tr
trPr = tr.get_or_add_trPr()
trPr.append(OxmlElement("w:cantSplit"))
if i < len(rows) - 1:
for cell in row.cells:
for paragraph in cell.paragraphs:
pPr = paragraph._element.get_or_add_pPr()
pPr.append(OxmlElement("w:keepNext"))
Call prevent_table_split(table) after populating every table. For very large tables (20+ rows) that genuinely cannot fit on one page, Word will still break them at a row boundary — these properties ensure it never breaks mid-row.
Bullets
Bullet character (•) with tab, hanging indent — never Word list bullets, never em-dash bullets:
<w:pPr>
<w:spacing w:line="276" w:lineRule="auto" w:after="120"/>
<w:ind w:left="720" w:hanging="360"/>
</w:pPr>
<w:r>...<w:t>• bullet text here</w:t></w:r>
For bold lead-in bullets, use separate runs: bullet+tab run, bold run (lead phrase), normal run (rest of text).
List Formatting Tips
- Colons after named entities in compressed lists. When a bullet lists items about a named entity and the list items themselves contain commas, use a colon after the entity name: "Center (Smith and Jones): research, conferences, student fellowships." This avoids ambiguity.
- Thematic sub-headings for long bullet lists. When a document has more than ~10 bullets, group them under bold italic sub-headings to prevent a "laundry list" feel. Sub-headings should be short (3-5 words) and thematic.
- Merging status categories. When a document has both accomplished and pipeline items, consider merging them under thematic headings with pipeline items marked by a leading "()" rather than separating into two sections. Add a legend: "Items marked () are in development."
Numbered lists: standard Arabic numerals for sequential/ordered items, same indent values.
Footer (multi-page documents)
Centered, Cambria 10pt italic, "Page x of y."
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
footer = section.footer
footer.is_linked_to_previous = False
p = footer.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
for text, field in [("Page ", None), (None, "PAGE"), (" of ", None), (None, "NUMPAGES"), (".", None)]:
run = p.add_run(text or "")
run.font.name = "Cambria"
run.font.size = Pt(10)
run.font.italic = True
if field:
fld = OxmlElement("w:fldSimple")
fld.set(qn("w:instr"), field)
run._element.append(fld)
Section Properties
<w:sectPr>
<w:pgSz w:w="12240" w:h="15840" w:orient="portrait"/>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"
w:header="708" w:footer="708" w:gutter="0"/>
<w:docGrid w:linePitch="360"/>
</w:sectPr>
Tone and Voice
Voice baseline (tone, banned phrases, preferred expressions) is defined in CLAUDE.md — that always applies. Documents layer on these additional conventions:
- More formal than emails but never bureaucratic — authoritative, well-organized institutional writing
- For committee-authored documents: "The [committee] recommends," "The [committee] proposes"
- Uses "I" or institutional voice consistently throughout — doesn't switch
- Organized with the most important information first (not buried in conclusions)
- Bullet lists always introduced by a full sentence
- No heading styles that feel like PowerPoint slides
- Brevity scales with audience knowledge. When the reader knows the institution, strip explanatory detail. Don't describe what they already know. A donor who sits on the board doesn't need to be told what CTIC is.
- AI-researched facts must be verified. When content originates from web scraping or AI research agents, every factual claim (names, dates, program names, affiliations, statistics) must be verified before inclusion. AI-generated research frequently invents plausible-sounding details.
Document Type Patterns
Proposal (most common)
- Purpose / Recommendation — one paragraph stating what is proposed and by whom
- Background / Context — brief (1-3 paragraphs); what led to this proposal
- The Proposal — substantive section; may use numbered or bulleted items for specific changes
- Rationale — why each element of the proposal makes sense; can be integrated into #3
- Implementation / Next Steps — what happens if approved; who does what
Report or Briefing Doc
- Summary — 2-4 sentence overview of findings/conclusions up front
- Background — context needed to understand the report
- Findings / Analysis — organized by section with bold headings
- Recommendations or Conclusions
- Appendices if needed (data, supporting materials)
White Paper / Policy Document
- Executive Summary (1-2 paragraphs)
- Issue / Problem Statement
- Analysis (organized sections)
- Recommendations
- Conclusion
File Production
Always use python-docx to generate .docx files. Write a Python script that builds
the document programmatically. Do NOT write markdown and convert with Pandoc — Pandoc
produces corrupted OOXML with duplicate style IDs, misnamed image files, and malformed
relationships that cause Word to refuse to open the file.
- Initialize with logo — load and insert the Penn Carey Law logo as the first element (see Logo section above)
- Build the title block appropriate to document type
- Apply Cambria 12pt throughout — headings are bold same-size, not larger
- Bullet character (•) for unordered lists; Arabic numerals for ordered lists
- Add footer with page numbers on multi-page documents
- Save to
~/Downloads/ (CLI) or /mnt/user-data/outputs/ (web)
- Run the Post-Generation Validation step (see below) before delivering
- Filename convention:
[DocType]_[Topic]_[YYYY-MM].docx (e.g., Proposal_StudentProjects_2025-11.docx)
AI Writing Tell Check
Before delivering any document, scan the full text for these common AI writing patterns. They signal machine-generated prose and undermine credibility. Fix every instance found.
Filler phrases to cut or replace:
- "a wide range of" — replace with "many," "diverse," or just drop it
- "a variety of" — same treatment
- "it is important to note that" / "it is worth noting that" — banned; cut entirely
- "taken together" — AI transition; just start with the conclusion
- "reflecting the breadth of" — wordy; say it directly
- "in a structured way" / "in a meaningful way" / "in a comprehensive manner" — filler; cut
- "the larger point is" — throat-clearing; lead with the point
Overused words to vary or cut:
- "several" — AI defaults to "several" when it doesn't know the count. If it appears more than once in a document, vary with "some," "a few," "a number of," or give the actual number
- "curated" — AI favorite; usually unnecessary
- "robust" — banned per CLAUDE.md; be specific about what makes something strong
- "landscape" — banned per CLAUDE.md
- "nuanced" — AI filler; say what the nuance actually is
- "multifaceted" — cut; describe the actual facets instead
- "leveraging" / "utilizing" — banned; use "using"
Structural tells:
- Identical sentence patterns repeated across consecutive paragraphs or bullets (e.g., every bullet starting with "This program..." or every paragraph opening with "The...")
- Trailing summary lists that restate what was just said ("spanning X, Y, Z, and W")
- Overwrought framing where plain language would do ("spans every stage of the J.D. program" vs. "from all three class years")
- Excessive parallel structure in prose (fine in bullet lists, robotic in paragraphs)
Em-dash overuse:
- More than 1-2 inline em-dashes per page signals AI. Replace most with commas. Keep em-dashes only for strong emphasis or to set off lists that contain commas.
This check applies to all output — documents, memos, emails, and any prose produced on the user's behalf.
Automated review: After writing the document file:
- If the
factual-reviewer agent is available, spawn it and pass it the file path. Fix any factual issues it flags.
- If the factual reviewer lists claims needing live verification, if the
fact-verifier agent is available, spawn it with those claims. Correct any contradicted claims; flag unverifiable ones for the user.
- If the
voice-style-checker agent is available, spawn it and pass it the file path. Fix any style issues it flags.
Complete all steps before delivering to the user.
Post-Generation Validation (Required)
After generating any .docx file, run this validation script to catch structural
issues that prevent Word from opening the file. This is mandatory — do not skip it.
import zipfile, re, os
def validate_and_repair_docx(filepath):
"""Check a .docx for common corruption issues and repair if needed."""
repairs = []
z = zipfile.ZipFile(filepath, 'r')
styles_xml = z.read('word/styles.xml').decode('utf-8')
pattern = r'<w:style\s[^>]*w:styleId="([^"]*)"'
style_ids = re.findall(pattern, styles_xml)
from collections import Counter
dupes = {k: v for k, v in Counter(style_ids).items() if v > 1}
if dupes:
repairs.append(f"Duplicate style IDs found: {list(dupes.keys())}")
seen = set()
full_pattern = r'(<w:style\s[^>]*w:styleId="([^"]*)"[^>]*>.*?</w:style>)'
def dedup(m):
sid = m.group(2)
if sid in seen:
return ''
seen.add(sid)
return m.group(1)
styles_xml = re.sub(full_pattern, dedup, styles_xml, flags=re.DOTALL)
styles_xml = re.sub(r'\n\s*\n', '\n', styles_xml)
bad_images = [n for n in z.namelist()
if n.startswith('word/media/rId') and n.endswith('.png')]
if bad_images:
repairs.append(f"Images with rId names found: {bad_images}")
if repairs:
tmp = filepath + '.tmp'
z_out = zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED)
for item in z.infolist():
if item.filename == 'word/styles.xml':
z_out.writestr(item, styles_xml.encode('utf-8'))
else:
z_out.writestr(item, z.read(item.filename))
z_out.close()
z.close()
os.replace(tmp, filepath)
print(f"REPAIRED {filepath}: {'; '.join(repairs)}")
else:
z.close()
print(f"VALID: {filepath}")
validate_and_repair_docx("path/to/output.docx")
If validation reports any repairs, that means the generation method produced a
corrupted file. Switch to python-docx and regenerate — do not rely on the
repair as the primary fix. The repair is a safety net, not a substitute for
correct generation.
Quick Checklist Before Delivering