# Penned AI playbook: research papers

Playbook version: 1.2 (23 October 2026)
Download the latest copy from https://penned-ai.vercel.app/playbook

This file is written for an AI agent (Claude, ChatGPT, Gemini, Cursor, Codex, Claude Code or any agent that can run scripts and read files). The person who gave it to you wants a complete research paper that reads as human-written, keeps every fact exactly right, and arrives as a finished PDF or Word file.

You will draft the paper, humanize the prose with the Penned API, restore any fact the rewrite got wrong with small exact patches, and then build the final paper **with a script** that copies the humanized text into the paper's structure. You never retype, edit, tidy or "improve" humanized text by hand. That rule is the reason this playbook exists. Read Part 0 before anything else.

---

## Part 0. Pinned rules (copy these into PROGRESS.md and re-read them before every phase)

1. **Humanized text is frozen.** Once text comes back from Penned, you never rewrite, rephrase, shorten, reflow, fix grammar in or "polish" it. Not one word. Any hand edit, even a small one, puts AI patterns back and undoes the work.
2. **Only scripts move humanized text.** The final paper is built by `scripts/assemble.py`, which reads the humanized files and copies them into the structure. You never paste humanized text into a file, a tool call or a chat message yourself.
3. **Facts are fixed with patches, not edits.** If the rewrite changed a fact, you add an exact find-and-replace patch to `patches.json` (Part 6). The script applies it. The humanized file on disk stays untouched.
4. **Only prose is humanized.** Headings, titles, author block, captions, tables, equations, code, lists of items, algorithm blocks and the reference list are written by you and never sent to Penned.
5. **Never invent.** No invented numbers, results, citations, DOIs, authors, datasets or quotes, and no claims the user's material does not make. If something is missing, ask the user or leave `[MISSING: ...]` in the draft and tell them. Every reference must pass `scripts/check_refs.py` (Appendix H); a reference you cannot verify is removed or marked for the user, never kept as if real.
6. **Follow the plan the user approved.** Do not change the structure, length or format they agreed to without asking.
7. **Keep PROGRESS.md current.** Update it after every step. If your context is reset, compressed or you start a new session, your first action is to read PROGRESS.md and this playbook again.
8. **End with the integrity statement** in Part 9, based on the verification report the script produces. Never claim it without running `scripts/verify.py`.

---

## Part 1. Do not lose this playbook mid-task

Long tasks make agents forget their instructions. Do all of the following:

- **Create `penned_work/PROGRESS.md` first**, before any other work, from the template in Appendix A. It holds the pinned rules, the phase checklist, the user's answers, decisions and the next step.
- **Re-read at every phase boundary.** Before starting each phase (Parts 3 to 9), re-read Part 0 and that phase's section of this playbook, then re-read PROGRESS.md. Write "Re-read playbook Part N" in PROGRESS.md when you do.
- **Gate each phase.** A phase is done only when every exit check listed for it is ticked in PROGRESS.md. Do not start the next phase with an open check.
- **Make the rule mechanical.** `scripts/humanize.py` saves every humanized file read-only and records its SHA-256 hash in `penned_work/manifest.json`. `scripts/verify.py` fails if any hash changes. If you ever find yourself about to open a humanized file in an editor, stop and re-read Part 0.
- **After a context reset**, never trust your memory of what was done. Trust PROGRESS.md, `manifest.json` and the files on disk.
- **If you are unsure about a step, re-read the relevant part of this file** rather than improvising.

---

## Part 2. What the user gives you

The user gives you this playbook, plus some of:

- a topic, an existing paper to extend, or their own project (code, notes, data, results, lab book);
- references (a .bib file, PDFs, DOIs or a list);
- a template from the venue (LaTeX class, Word template) or just its name;
- figures, data files, tables;
- extra instructions;
- a Penned API key (from https://penned-ai.vercel.app/app/keys). If they did not give one, ask for it. The key goes in an environment variable (`PENNED_API_KEY`) and never into any file you write, any log or the final output.

People do not only write papers about code. A paper may be a review of a topic, an extension of a published paper, an empirical study, a case study, a theoretical piece or a write-up of a project. Adapt the plan to what they are doing.

---

## Part 3. Ask before you build

Read everything the user attached first, so you do not ask about things already answered. Then ask the questions below **in one message**, grouped and numbered, each with your suggested default in brackets so the user can reply "defaults are fine" to the ones they do not care about. Skip a question if the attachments already answer it. Ask follow-ups only if an answer is unclear.

**A. The paper**
1. What kind of paper is it? A new paper on a topic, an extension of an existing paper (which one, and what is new), a write-up of your own project or experiments, a review or survey, or something else?
2. Working title, or should I propose three?
3. Main contribution or argument in one or two sentences, in your words. [I'll draft one from your material for you to approve]
4. Target venue or course, if any (conference, journal, university assignment). [none]

**B. Length and format**
5. Length: how many pages (and does that include references?) or how many words? [8 pages including references]
6. Format: LaTeX or Word? [LaTeX if a LaTeX template exists, otherwise Word]
7. Template or style: IEEE, Springer LNCS, ACM, Elsevier, APA, your university's template, or another? Attach it if you have it. [Springer LNCS]
8. Final output: PDF, Word (.docx), LaTeX source (Overleaf-ready zip), or several? [PDF plus the source]
9. Citation style: numeric [1], author-year, or as the template says? [as the template says]
10. Spelling: British or American English? [match your material]

**C. Content**
11. Sections: standard (Abstract, Introduction, Related Work, Method, Experiments or Results, Discussion, Conclusion) or a structure you need? Any required sections such as keywords, acknowledgments, ethics, data availability or disclosure of interests? [standard, plus what the template requires]
12. Figures: which ones should be included (attach them or point to the data), or should I decide? May I generate plots from your data? [I decide, and I generate plots only from your data]
13. Tables: which ones, or should I decide? [I decide]
14. Equations or algorithms needed? [only if your material has them]
15. References: only the ones you gave me, or may I add more? If I add any, I will only use ones I can verify exist (real DOI or publisher page) and list them for you to check. Roughly how many references? [only yours]
16. For an extended paper: should text from the original paper be reused at all? [No. I rewrite in new words, since copied sentences raise the similarity score]

**D. People and limits**
17. Authors, affiliations, emails, ORCID, acknowledgments and funding, exactly as they should appear. [I leave placeholders]
18. Is there any text you wrote yourself that must stay exactly as it is and not be humanized? [none]
19. Limits your institution checks: maximum AI score, maximum similarity (for example below 10% excluding bibliography), page or word limits, deadline? [none given]
20. Your Penned API key, if you have not given it yet. Create one at https://penned-ai.vercel.app/app/keys. The free plan allows 50 API calls a day of up to 1,200 words each.

Record every answer in PROGRESS.md under "User answers". Exit check: all answers recorded, or defaults accepted.

---

## Part 4. Plan and estimate (show the user before writing)

Write `penned_work/plan.md` and show it to the user. It contains:

1. **Title and one-paragraph summary** of the paper's argument.
2. **Outline**: every section and subsection with a target word count and a one-line description of what goes in it, which source material it uses and which references it cites.
3. **Non-prose items**: every figure, table, equation and algorithm with its number, caption draft and where its content comes from.
4. **Structure file**: `penned_work/structure.json`, the ordered list of every block in the paper (format in Part 7). This is what the assembly script follows.
5. **Word budget check**: total prose words against the page or word limit. As a guide, a single-column LNCS page holds about 400 to 450 words of prose; a two-column IEEE page about 800 to 900; a double-spaced A4 page about 250 to 300. Tables and figures take space.
6. **Penned calls needed**: number of prose blocks (Part 5) against the 50 calls per day limit.
7. **Time estimate**, broken down, for example:
   - drafting: about 3 to 5 minutes per 1,000 words;
   - humanizing: about 1 to 4 minutes per Penned call (calls can run in sequence while you prepare other blocks);
   - fact review and patches: about 5 minutes per section;
   - figures and tables: about 5 minutes each;
   - assembly, build and verification: about 15 to 30 minutes;
   - a total, for example "about 1.5 to 2.5 hours for an 8-page paper".
   Be honest. Say what could make it longer (missing data, API limits, a template that fights the layout).
8. **Open questions or risks**, such as missing results or unverifiable references.

Ask the user to approve or change the plan. Do not start drafting until they approve, unless they told you to go ahead without approval. Exit check: plan approved and recorded in PROGRESS.md.

---

## Part 5. Draft with the research-paper prompt

Draft the paper yourself following the research-paper prompt in Appendix B. Treat it as your writing instructions: the facts rules, the organisation rules, the sentence rules and the banned words all apply to everything you draft. The user's material is "my notes, results and sources" in that prompt.

How to lay the draft out on disk:

- One folder `penned_work/draft/`.
- **Prose blocks**: each run of consecutive prose paragraphs inside one section or subsection, with nothing non-prose between them, is one prose block, saved as `draft/<block-id>.txt` with paragraphs separated by one blank line. Block IDs follow the paper order, for example `03-method-02.txt`.
  - A prose block must be **40 to 1,100 words** (Penned's limit is 1,200 words per call). Split longer runs at a paragraph boundary into two blocks. A run under 40 words (a one-line bridge sentence) is written as a non-prose block and is not humanized; note it in PROGRESS.md.
  - Prose only: no headings, no LaTeX commands, no Markdown, no bullet points inside prose blocks.
  - **Citations inside prose** are written as numeric markers like `[4]` or `[4, 7]`, numbered by your reference list, even if the final style is author-year. The assembly script converts them. Penned checks numbers, so numeric markers survive the rewrite far better than `\cite{key}` commands.
  - **No inline math or LaTeX** in prose blocks. Write small expressions in plain text ("C = 0.01", "p < 0.05"). Anything that needs real math goes in an equation block.
  - Figure and table references in prose are written plainly: "Table 2", "Fig. 3".
- **Non-prose blocks** (headings, captions, tables, equations, algorithms, lists, author block, references, short bridges) go in `draft/nonprose/` in the final format (LaTeX or plain text for Word). They are never sent to Penned.
- Keep `draft/references.bib` (or `references.json`) with every reference and its number.
- **References come from the user's material first.** If the user gave a reference list, use it exactly as given (same titles, venues, years and DOIs) and do not "correct" it from memory. Only add a reference when the user allowed it (question 15), and only after `scripts/check_refs.py` finds it. Language models misremember venues, years and page numbers even for real papers, so a reference written from memory is treated as unverified until the script confirms it.
- **The draft says only what the material says.** Do not add explanations, mechanisms, protocols, laws, product details or results the material does not contain, even when they sound right. Do not turn a hedged or negative result into a positive one ("did not change the speaker-level decisions" must not become "prevented collapse"), and do not strengthen claims ("we prove", "accuracy collapses to chance") beyond what the numbers show.
- **The banned words in Appendix B also apply to headings, captions, table text and lists**, which Penned never rewrites. "Paradox", "reveal", "prove", "blind spot" and their kind in a heading read as AI as much as in prose. Contributions go in prose unless the template or the user wants a list.

Exit checks:
- every block in `structure.json` has its file;
- `scripts/trace_numbers.py` (Appendix I) lists no number in the draft that is missing from the user's material, or every one it lists is explained in PROGRESS.md;
- `scripts/check_refs.py` (Appendix H) verified every reference, or the unverified ones are marked for the user;
- every citation marker maps to a reference in the list, and every reference in the list is cited;
- section, table, figure and task numbering is consecutive and every "Table N" / "Fig. N" in the text exists;
- the word budget fits.

---

## Part 6. Humanize with Penned, then fix facts with patches

### 6.1 Humanize every prose block with a script

Use `scripts/humanize.py` (Appendix C). For each prose block it:

1. starts a rewrite job with `POST https://penned-ai.vercel.app/v1/jobs`, body `{"text": "...", "category": "academic"}` and header `X-API-Key: $PENNED_API_KEY`, then checks `GET https://penned-ai.vercel.app/v1/jobs/<job>` every few seconds until it is done (a rewrite takes 1 to 3 minutes);
2. saves the full JSON response to `penned_work/humanized/<block-id>.json`;
3. saves the `output` text to `penned_work/humanized/<block-id>.txt`, makes it read-only and records its SHA-256 hash in `manifest.json`.

It skips blocks already done, so it is safe to re-run after an error. It waits and retries on HTTP 503 and on 429 rate limits. If the response says the daily quota is used up, it stops; tell the user how many blocks are left and that the quota resets at midnight UTC.

The whole paper should be humanized, including the abstract and conclusion. Detectors judge the whole document, so one machine-sounding section raises the score for everything.

### 6.2 Review the facts

Penned checks every rewrite against your draft and restores numbers, names, units, counts and claims itself, so most blocks need nothing. Its JSON response includes `review`: a list of notes for paragraphs where it found something to look at, and `numbers_kept`.

Run `scripts/factcheck.py` (Appendix D). It compares each humanized block with its draft block and lists, per block:
- numbers, percentages and units in the draft that are missing or different in the rewrite;
- numbers in the rewrite that are not in the draft;
- citation markers lost, added or moved to a different sentence;
- names, datasets, model names and terms in the draft that disappeared, and capitalised names that appeared from nowhere;
- Penned's own `review` notes.

Then read each humanized block once next to its draft, looking only for facts: a number or unit changed, a citation moved to the wrong claim, someone else's result turned into "we" (or the reverse), a claim made stronger or weaker ("suggests" became "proves"), a negation flipped, a technical term swapped for a synonym. Style is not your concern here. Do not "fix" wording you merely dislike.

### 6.3 Restore facts with exact patches

For every real fact error, add a patch to `penned_work/patches.json`:

```json
[
  {"block": "04-results-01", "find": "accuracy of 72.3%", "replace": "accuracy of 72.30%",
   "reason": "draft says 72.30%"},
  {"block": "02-related-01", "find": "We showed that", "replace": "Kim et al. [6] showed that",
   "reason": "finding belongs to [6], not to us"}
]
```

Patch rules:
- `find` must occur **exactly once** in that block's humanized text. Include a few neighbouring words to make it unique.
- Change **as few words as possible**: the number, the unit, the name, the citation marker, the hedge word. Keep the rest of the sentence as Penned wrote it.
- If a whole point was dropped and cannot be restored with a small patch, the patch may replace the one sentence that lost it with the matching sentence from the draft. Never more than one sentence per patch.
- Every patch has a `reason` that names the fact.
- If one block needs patches touching more than about 10% of its words, do not patch it. Re-humanize that block instead: delete its row from `manifest.json` together with its two files, check the draft block is factually right, and run `humanize.py` again for that block. Record this in PROGRESS.md.

The humanized `.txt` files are never edited. `assemble.py` applies the patches as it builds and fails loudly if a `find` string is missing or appears more than once.

Exit checks: every block humanized; `factcheck.py` output reviewed for every block; every real fact error has a patch; PROGRESS.md lists blocks with patches and blocks re-humanized.

---

## Part 7. Assemble the paper with a script (the most important step)

**This is the step that decides whether the paper stays human.** The humanized text must reach the final paper exactly as Penned wrote it, with only the fact patches applied. Agents that copy text across by hand always change it: they fix a comma, merge two sentences, "smooth" a transition, retype a number. Each of those changes brings AI patterns back. So you write a script, and the script does the copying.

### 7.1 The structure file

`penned_work/structure.json` lists every block of the paper in order:

```json
{
  "format": "latex",
  "template": "llncs",
  "blocks": [
    {"type": "raw", "file": "draft/nonprose/00-preamble.tex"},
    {"type": "abstract", "id": "00-abstract-01"},
    {"type": "heading", "level": 1, "text": "Introduction"},
    {"type": "prose", "id": "01-intro-01"},
    {"type": "raw", "file": "draft/nonprose/01-fig-pipeline.tex"},
    {"type": "prose", "id": "01-intro-02"},
    {"type": "heading", "level": 2, "text": "Datasets"},
    {"type": "prose", "id": "03-method-01"},
    {"type": "raw", "file": "draft/nonprose/03-table-datasets.tex"},
    {"type": "references", "file": "draft/references.bib"},
    {"type": "raw", "file": "draft/nonprose/99-end.tex"}
  ]
}
```

- `prose` and `abstract` blocks name a humanized block ID. The script reads `humanized/<id>.txt`, applies that block's patches, and inserts it.
- `heading`, `raw` and `references` blocks hold content you wrote. They are never humanized.

### 7.2 What `assemble.py` does (Appendix E has a working version)

1. Loads `structure.json`, `patches.json` and `manifest.json`.
2. For each prose block: reads the humanized text, checks its hash against the manifest, applies its patches, then splits it into paragraphs on blank lines.
3. Makes only **mechanical, format-level** changes that do not alter a single word:
   - for LaTeX: escapes `& % $ # _ { } ~ ^ \`; turns straight quotes into LaTeX quotes; turns `[4, 7]` into `\cite{key4,key7}` using the reference map;
   - for Word: places each paragraph in a paragraph with the body style of the template; turns `[4]` into the citation style the user chose.
4. Writes `build/main.tex` (or `build/paper.docx`), plus a record `build/assembly_log.json` listing which block went where and which patches were applied.
5. Never wraps, reflows, merges or splits sentences, and never changes word order or wording.

You may change the script's code as much as you need to fit the template. You may not change the text it carries. If the output looks wrong (a paragraph missing, a heading in the wrong place), fix `structure.json` or the script and run it again. Never fix the output file by hand.

### 7.3 Lessons from real runs

- **Map blocks by ID, never by position or paragraph count.** Penned can merge or split paragraphs inside a block, so the number of paragraphs that comes back may differ from the draft. Insert the whole block at its place.
- **Keep the PDF's text clean for detectors and plagiarism checkers.** Avoid running headers that repeat the title or author names on every page (for example, do not use `\documentclass[runningheads]{llncs}`). They get mixed into the extracted text and make the document look more machine-generated to AI detectors.
- **Do not hyphenate or justify by hand.** Let LaTeX or Word do the layout.
- **Do not add text in the build step.** No captions, footnotes or transition sentences invented at build time. Anything new goes back through the draft (and, if it is prose of 40 words or more, through Penned).

Exit checks: `assemble.py` runs with no error; every prose block in `structure.json` was inserted; no patch failed.

---

## Part 8. Build the final output and verify it

1. **Build.**
   - LaTeX: compile with `latexmk -pdf` (or `pdflatex` plus `bibtex` twice). Fix only LaTeX errors in the template, the non-prose files or the script, never in the humanized text. If the user wants Overleaf, zip `build/` with the class files, figures and `.bib`.
   - Word: build with `python-docx` from the venue's template if there is one, or with Pandoc from a reference .docx. Export to PDF with LibreOffice (`soffice --headless --convert-to pdf`) if the user wants a PDF.
2. **Check the layout.** Open the PDF, or render pages to images, and look at every page: figures and tables placed and numbered correctly, nothing cut off, page limit met, references formatted. In particular:
   - **No table or figure runs past the margin.** For LaTeX, search the log for `Overfull \hbox` warnings larger than 2pt and fix each one (shorter column headers, `\small`, `tabularx`, `\resizebox{\textwidth}{!}{...}`, or splitting the table). A cut-off column is a fail.
   - **Every caption describes its own image.** Look at each figure and read its caption. A caption written for a planned diagram must not sit under a bar chart.
   - **Every table agrees with the text** that quotes it, and repeated numbers (the same baseline in two tables) are identical everywhere.
3. **Run `scripts/verify.py`** (Appendix F). It:
   - re-checks every humanized file's hash against `manifest.json`;
   - extracts the text of the final PDF or .docx;
   - checks that every humanized paragraph, after patches, appears in the final text word for word (ignoring line breaks, hyphenation at line ends and LaTeX quote marks);
   - counts the words that differ from Penned's output, other than the patches;
   - writes `penned_work/VERIFY_REPORT.md` with the results.
   The run passes only if all hashes match and 100% of humanized paragraphs are found unchanged. If it fails, fix the script or structure and rebuild. Never "fix" it by editing text.
4. **Run `scripts/trace_numbers.py` and `scripts/check_refs.py` again on the final text**, since tables, captions and references were written outside Penned.
5. **Optional checks.** You can score the final text with `POST https://penned-ai.vercel.app/v1/detect` (same key; one call per 1,200 words). Tell the user the score is an estimate, and that their institution's checker (Turnitin, GPTZero) is the one that counts.

Exit checks: output files built; no overfull boxes over 2pt; every caption checked against its image; `verify.py`, `trace_numbers.py` and `check_refs.py` passed.

---

## Part 9. Deliver and state how it was built

Give the user:

1. The final files (PDF, .docx, LaTeX source or Overleaf zip, as agreed).
2. A short list of anything they must check or fill in: `[MISSING: ...]` placeholders, references you added (with DOIs), references `check_refs.py` could not verify, numbers `trace_numbers.py` could not trace, and assumptions you made.
3. The patch list from `patches.json`, in a short readable form, so they can see every fact you restored.
4. **The integrity statement**, using the real numbers from `VERIFY_REPORT.md`:

> **How this paper was assembled.** I drafted the prose, humanized all of it with Penned ({N} blocks, {W} words), and built the final paper with a script (`scripts/assemble.py`) that copied the humanized text into the paper's structure. I did not edit, rephrase or retype the humanized text by hand. The only changes to it are {P} fact patches listed in `patches.json`, each restoring a number, name, citation or claim from the draft. `scripts/verify.py` confirmed that all {N} humanized blocks appear in the final {PDF/document} unchanged apart from those patches, and that none of the humanized files changed after Penned returned them.

Only write this statement if `verify.py` passed. If it did not pass, say so plainly and say what is different.

Finally, recommend that the user reads the whole paper once before submitting, and runs their institution's similarity and AI checks (with "exclude bibliography" on for similarity).

---

## Appendix A. PROGRESS.md template

```markdown
# PROGRESS (read this first after any break or context reset)

Playbook: penned-research-paper-playbook.md (re-read Part 0 and the current phase's part before each phase)

## Pinned rules
1. Humanized text is frozen. Never edit it by hand.
2. Only scripts move humanized text (assemble.py).
3. Facts are fixed with patches in patches.json, never by editing humanized files.
4. Only prose is humanized. Headings, captions, tables, equations, references: never.
5. Never invent numbers, results, claims, citations or references. check_refs.py must verify every reference.
6. Follow the approved plan.
7. Update this file after every step.
8. End with the integrity statement, only after verify.py passes.

## Phase checklist
- [ ] Part 3  Questions asked, answers recorded
- [ ] Part 4  Plan + structure.json + time estimate approved
- [ ] Part 5  Draft complete, numbers traced, references verified, word budget fits
- [ ] Part 6  All blocks humanized, fact review done, patches written
- [ ] Part 7  assemble.py runs clean
- [ ] Part 8  Built, no overfull boxes, captions match images, verify.py + trace_numbers.py + check_refs.py passed
- [ ] Part 9  Delivered with integrity statement

## User answers

## Decisions and assumptions

## Blocks (id, words, humanized?, patches)

## Log (newest last; one line per step, include "Re-read playbook Part N")

## Next step
```

---

## Appendix B. The research-paper prompt (your drafting instructions)

Use this as your instructions for drafting every prose block. Where it says "I" or "my notes", that is the user and their material.

```text
Write the research paper text I ask for below the way the researcher who did the work would draft it by hand, not the way an AI assistant writes. I will give you my notes, results and sources. Detectors flag research writing mostly because of how AI organises and packs it, not only because of its word choice, so the structure rules below matter as much as the word rules.

FACTS COME FIRST. Never trade a fact for style.
- Use only the facts, numbers, datasets, methods, names and sources I give you. Copy every number exactly, with its unit, sign and decimal places. Never round, convert, recompute or "tidy" a number.
- Keep each citation attached to the exact claim it supports, in the format I use. Do not add citations, authors, years or papers I did not give you. If a sentence needs a source I did not give, write [CITE] and move on.
- Keep who did what. Other people's results stay theirs ("Smith et al. [4] found..."). Only my own work gets "we". Never present a prior finding as ours, or ours as someone else's.
- Keep the strength of every claim. "Improves" never becomes "solves", "suggests" never becomes "shows", "in our sample" never becomes "in general", and a decrease never becomes an increase. If my notes hedge, keep the hedge.
- Keep technical terms, model names, dataset names, metric names and hyphenated terms exactly as I wrote them. Use the same term every time. Do not swap in synonyms or invent new abbreviations.
- If something in my notes is unclear or missing, leave a short note in [square brackets] instead of guessing.

HOW TO ORGANISE IT
- Lower the density. AI-written papers pack a new claim, number or term into every sentence. Real drafts do not. Let some sentences simply say why a step was needed, what you expected, or what surprised you.
- Put at most two numbers in any sentence. When a passage would need many numbers (results across models, datasets, folds or settings), put them in a table and mention in the text only the two or three that matter for the point you are making, then point to the table.
- Order material by the argument, not as a list of facts. In related work, group papers by the point they support or the gap they leave, and say in a few words why each one matters to this study. Never write one paper per sentence in the same template ("X et al. did A. Similarly, Y et al. did B. Z et al. extended this to C.").
- Methods: say what you did in the order you did it, in plain past tense, with the practical reasons behind choices when my notes give them ("we dropped clips shorter than one second because most were silence").
- Results: give the main result plainly first, then the exceptions and what did not work. Do not present every result as a striking finding.
- Discussion: interpret in a measured voice. Say what you think explains a result and how sure you are. Mention an alternative explanation when my notes give one.
- Introduction: open with the concrete problem this work deals with, not a sweeping statement about how important the field is. State the contributions in prose unless I ask for a list.
- Abstract: one plain paragraph. The problem, what we did, two or three key numbers, what it means. No dramatic framing, no "This study presents X, a novel...", and not every number from the paper.
- Limitations: plain and specific. Say what was not tested and why it matters.
- Paragraphs should be uneven: some two or three sentences, some much longer. Break paragraphs where the topic actually changes, not after every point.
- Do not end paragraphs or sections with a sentence that restates them. Only the conclusion summarises.
- No roadmaps ("This paper is organised as follows", "In this section, we", "First... Second... Finally..."), unless I ask for the standard organisation paragraph.
- Write each section to fit its own content. Sections should not all follow the same shape.

HOW TO WRITE THE SENTENCES
- Vary sentence length a lot. Some sentences 6 to 10 words, some 30 or more with a clause or an aside. Never three sentences of similar length in a row.
- Use "we" for the authors. Mix active ("we trained") and passive ("the models were trained"), as real papers do.
- Repeat a key term instead of rotating synonyms for it. People repeat their terms; AI keeps varying them.
- Unpack long noun stacks into a clause where you can ("validation that keeps each speaker in one fold" rather than a five-word stacked label), but keep any named method exactly as written.
- Prefer plain verbs: use, show, find, test, train, compare, drop, keep, miss. Not leverage, utilize, facilitate, demonstrate (in every sentence), exhibit, elucidate.
- Hedge where a careful researcher is actually unsure: "suggests", "in our data", "we suspect", "as far as we know". Not in every sentence.
- Some roughness is fine: a plain sentence like "This was a problem." or "We did not expect this.", a slightly long sentence, a less-than-elegant transition. Do not polish every line to the same finish.

DO NOT USE
- Dramatic or coined labels for findings ("paradox", "failure modes", "blind spot", "double-edged sword"), and verbs like exposes, reveals, uncovers, unlocks, sheds light on, paves the way, cripples, plagues, hampers.
- Inflated words: novel (unless I say it is), crucial, pivotal, vital, robust, comprehensive, systematic, holistic, seamless, notable, remarkable, unprecedented, intricate, nuanced, multifaceted, landscape, realm, paradigm, delve, underscore, highlight (as a verb), showcase, harness, bolster, foster. A word that is a technical term in my notes (for example "severe" as a clinical grade, or "robust" as a named statistical method) is fine.
- "Significant" or "significantly" unless it refers to a statistical test I gave you.
- Sentence-opening connectors: Furthermore, Moreover, Additionally, Notably, Importantly, Consequently, Thus, Hence, Overall, In conclusion, To this end, In this regard.
- Stock phrases: "plays a crucial role", "has gained significant attention", "a growing body of literature", "in recent years", "with the advent of", "to address this gap", "remains an open challenge", "it is worth noting", "it is important to note", "a promising direction", "holds great promise", "real-world applications", "state-of-the-art" (unless comparing to a named method), "opens new avenues", "future work could explore".
- Sentence after sentence opening with "While X, Y" or "Although X, Y"; "not only X but also Y"; "X, Y, and Z" lists of three used for rhythm (real lists of items are fine).
- Em dashes, semicolons, colons that set up a reveal, rhetorical questions, exclamation marks, bold or italics for emphasis, and bullet points inside prose sections.

BEFORE YOU ANSWER, CHECK
- Every number, citation, name and term matches my notes exactly, and nothing was added.
- Every finding is attributed to the right people.
- No sentence has more than two numbers.
- No paragraph ends with a summary line, and none of the words or phrases above appear.

Write it straight through as one draft. Do not write a polished version and then "roughen" it. Output only the paper text, with the section headings I use. No notes to me and no title unless I ask.

What I want you to write (the section, my notes, results and sources):
```

---

## Appendix C. scripts/humanize.py

```python
"""Humanize every prose block with Penned. Safe to re-run: finished blocks are skipped.
Usage: PENNED_API_KEY=... python scripts/humanize.py [block-id ...]"""
import hashlib, json, os, pathlib, stat, sys, time
import requests

API = "https://penned-ai.vercel.app/v1/jobs"
ROOT = pathlib.Path("penned_work")
KEY = os.environ.get("PENNED_API_KEY") or sys.exit("Set PENNED_API_KEY first.")
H = {"X-API-Key": KEY}

structure = json.loads((ROOT / "structure.json").read_text())
manifest_path = ROOT / "manifest.json"
manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
(ROOT / "humanized").mkdir(exist_ok=True)


def humanize(text):
    """Start a rewrite job and wait for it. Returns the finished job (output, scores, review notes)."""
    for attempt in range(6):
        r = requests.post(API, json={"text": text, "category": "academic"}, headers=H, timeout=60)
        if r.status_code == 429 and "today's" in r.text:
            sys.exit("Daily Penned quota used. It resets at midnight UTC.")
        if r.status_code in (429, 502, 503, 504):
            time.sleep(30 * (attempt + 1))
            continue
        r.raise_for_status()
        break
    else:
        sys.exit("Penned did not accept the job after retries. Re-run later.")
    job = r.json()["job"]
    while True:
        time.sleep(5)
        s = requests.get(f"{API}/{job}", headers=H, timeout=60)
        if s.status_code in (502, 503, 504):
            continue
        s.raise_for_status()
        data = s.json()
        if data["status"] == "done":
            return data
        if data["status"] == "failed":
            sys.exit(f"The rewrite failed: {data.get('error')}. Re-run this script to retry the block.")


wanted = set(sys.argv[1:])
ids = [b["id"] for b in structure["blocks"] if b["type"] in ("prose", "abstract")]
for bid in ids:
    if (wanted and bid not in wanted) or bid in manifest:
        continue
    text = (ROOT / "draft" / f"{bid}.txt").read_text(encoding="utf-8").strip()
    words = len(text.split())
    if not 40 <= words <= 1200:
        sys.exit(f"{bid}: {words} words. Blocks must be 40 to 1,200 words; split or merge it in the draft.")
    data = humanize(text)
    (ROOT / "humanized" / f"{bid}.json").write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
    out = ROOT / "humanized" / f"{bid}.txt"
    out.write_text(data["output"].strip() + "\n", encoding="utf-8")
    out.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)  # read-only: humanized text is frozen
    manifest[bid] = {"sha256": hashlib.sha256(out.read_bytes()).hexdigest(), "words_in": words,
                     "words_out": len(data["output"].split()), "ai_after": data.get("ai_score_after"),
                     "review_notes": len(data.get("review", []))}
    manifest_path.write_text(json.dumps(manifest, indent=2))
    print(f"{bid}: done, AI score {data.get('ai_score_after')}, {len(data.get('review', []))} review notes")
```

---

## Appendix D. scripts/factcheck.py

```python
"""List possible fact changes between each draft block and its humanized version. It only reports; it changes nothing.
Usage: python scripts/factcheck.py > penned_work/factcheck.txt"""
import json, pathlib, re

ROOT = pathlib.Path("penned_work")
NUM = re.compile(r"(?<![\w.])[-+−]?\d[\d,]*(?:\.\d+)?\s*(?:%|pp|percentage points?|ms|s|Hz|kHz|MB|GB|dB)?")
CITE = re.compile(r"\[(\d+(?:\s*[,–-]\s*\d+)*)\]")
NAME = re.compile(r"\b(?:[A-Z][a-zA-Z0-9]*[A-Z0-9][a-zA-Z0-9-]*|[A-Z][a-z]+(?:[ -][A-Z][a-z]+)+)\b")

def norm(n):
    return re.sub(r"\s+", "", n.replace(",", "").replace("−", "-"))

manifest = json.loads((ROOT / "manifest.json").read_text())
for bid in manifest:
    draft = (ROOT / "draft" / f"{bid}.txt").read_text(encoding="utf-8")
    human = (ROOT / "humanized" / f"{bid}.txt").read_text(encoding="utf-8")
    review = json.loads((ROOT / "humanized" / f"{bid}.json").read_text()).get("review", [])
    d_num, h_num = {norm(x) for x in NUM.findall(draft)}, {norm(x) for x in NUM.findall(human)}
    d_cite, h_cite = CITE.findall(draft), CITE.findall(human)
    d_name, h_name = set(NAME.findall(draft)), set(NAME.findall(human))
    issues = []
    if d_num - h_num: issues.append(f"numbers missing or changed: {sorted(d_num - h_num)}")
    if h_num - d_num: issues.append(f"numbers not in draft: {sorted(h_num - d_num)}")
    if sorted(d_cite) != sorted(h_cite): issues.append(f"citations draft {d_cite} vs humanized {h_cite}")
    if d_name - h_name: issues.append(f"names/terms missing: {sorted(d_name - h_name)}")
    if h_name - d_name: issues.append(f"names/terms not in draft: {sorted(h_name - d_name)}")
    for note in review:
        issues.extend(f"Penned note: {n}" for n in note.get("notes", []))
    print(f"== {bid}: {'OK' if not issues else str(len(issues)) + ' to check'}")
    for i in issues:
        print("   -", i)
```

The script is a first filter. You still read each block against its draft for attribution ("we" versus others), claim strength, negations and citations attached to the wrong claim, which no regex can catch.

---

## Appendix E. scripts/assemble.py (LaTeX version; adapt the writer for Word)

```python
"""Build the paper from structure.json. Copies humanized text exactly, applies fact patches, escapes for LaTeX.
It never rewrites wording. Usage: python scripts/assemble.py"""
import hashlib, json, pathlib, re, sys

ROOT = pathlib.Path("penned_work")
S = json.loads((ROOT / "structure.json").read_text())
manifest = json.loads((ROOT / "manifest.json").read_text())
patches = json.loads((ROOT / "patches.json").read_text()) if (ROOT / "patches.json").exists() else []
refmap = json.loads((ROOT / "draft" / "refmap.json").read_text())  # {"4": "joshy2022", ...}
log, used = [], set()

def humanized(bid):
    path = ROOT / "humanized" / f"{bid}.txt"
    if hashlib.sha256(path.read_bytes()).hexdigest() != manifest[bid]["sha256"]:
        sys.exit(f"{bid}: humanized file changed after Penned returned it. Restore it; never edit it.")
    text = path.read_text(encoding="utf-8").strip()
    for i, p in enumerate(patches):
        if p["block"] != bid:
            continue
        n = text.count(p["find"])
        if n != 1:
            sys.exit(f"patch {i} ({bid}): 'find' occurs {n} times; it must occur exactly once.")
        text = text.replace(p["find"], p["replace"])
        used.add(i)
    return text

TEX = {"\\": r"\textbackslash{}", "&": r"\&", "%": r"\%", "$": r"\$", "#": r"\#", "_": r"\_",
       "{": r"\{", "}": r"\}", "~": r"\textasciitilde{}", "^": r"\textasciicircum{}"}

def to_tex(text):
    text = "".join(TEX.get(c, c) for c in text)
    text = re.sub(r"\"(\S[^\"]*?)\"", r"``\1''", text)          # straight double quotes to TeX quotes
    def cite(m):
        nums = []
        for part in re.split(r"\s*,\s*", m.group(1)):
            ends = [int(x) for x in re.split(r"\s*[–-]\s*", part)]
            nums += [str(k) for k in range(ends[0], ends[-1] + 1)]
        return r"~\cite{" + ",".join(refmap[n] for n in nums) + "}"
    text = re.sub(r"\s*\[(\d+(?:\s*[,–-]\s*\d+)*)\]", cite, text)
    return "\n\n".join(p.strip() for p in re.split(r"\n\s*\n", text) if p.strip())

out = []
for b in S["blocks"]:
    t = b["type"]
    if t == "prose":
        out.append(to_tex(humanized(b["id"])))
    elif t == "abstract":
        out.append("\\begin{abstract}\n" + to_tex(humanized(b["id"])) + "\n\\end{abstract}")
    elif t == "heading":
        out.append({1: "\\section", 2: "\\subsection", 3: "\\subsubsection"}[b["level"]] + "{" + b["text"] + "}")
    elif t == "raw":
        out.append((ROOT / b["file"]).read_text(encoding="utf-8").strip())
    elif t == "references":
        out.append("\\bibliographystyle{" + S.get("bibstyle", "splncs04") + "}\n\\bibliography{references}")
    log.append({"type": t, "id": b.get("id"), "file": b.get("file")})

unused = [i for i, p in enumerate(patches) if i not in used]
if unused:
    sys.exit(f"patches {unused} belong to blocks that are not in structure.json.")
(ROOT / "build").mkdir(exist_ok=True)
(ROOT / "build" / "main.tex").write_text("\n\n".join(out) + "\n", encoding="utf-8")
(ROOT / "build" / "assembly_log.json").write_text(json.dumps({"blocks": log, "patches_applied": sorted(used)}, indent=2))
print(f"Wrote build/main.tex: {sum(1 for x in log if x['id'])} humanized blocks, {len(used)} patches.")
```

For Word, keep the same `humanized()` function and replace `to_tex` with code that adds each paragraph to a `python-docx` document built from the template (`doc.add_paragraph(text, style="Body Text")` or the template's body style), and headings with `doc.add_heading`. Citations become "[4]" or "(Author, Year)" as agreed.

---

## Appendix F. scripts/verify.py

```python
"""Check that the final output contains every humanized block exactly (after patches) and nothing was hand-edited.
Usage: python scripts/verify.py build/main.pdf   (or build/paper.docx)"""
import hashlib, json, pathlib, re, subprocess, sys

ROOT = pathlib.Path("penned_work")
final = pathlib.Path(sys.argv[1])
manifest = json.loads((ROOT / "manifest.json").read_text())
patches = json.loads((ROOT / "patches.json").read_text()) if (ROOT / "patches.json").exists() else []

if final.suffix == ".pdf":
    raw = subprocess.run(["pdftotext", "-nopgbrk", str(final), "-"], capture_output=True, text=True).stdout
else:
    import docx
    raw = "\n".join(p.text for p in docx.Document(str(final)).paragraphs)

def words(s):
    s = re.sub(r"(\w)-\n(\w)", r"\1\2", s)                       # line-end hyphenation in PDFs
    s = s.replace("``", '"').replace("''", '"').replace("“", '"').replace("”", '"').replace("’", "'")
    s = s.replace("ﬁ", "fi").replace("ﬂ", "fl").replace("ﬀ", "ff").replace("ﬃ", "ffi").replace("_", " ")
    s = re.sub(r"\[\d+(?:\s*[,–-]\s*\d+)*\]|\(\w[^()]*\d{4}[a-z]?\)", " ", s)  # citations change form at build
    return re.findall(r"[\w'%.]+", s.lower())

final_words = " ".join(words(raw))
rows, ok = [], True
for bid, rec in manifest.items():
    path = ROOT / "humanized" / f"{bid}.txt"
    same_hash = hashlib.sha256(path.read_bytes()).hexdigest() == rec["sha256"]
    text = path.read_text(encoding="utf-8")
    for p in patches:
        if p["block"] == bid:
            text = text.replace(p["find"], p["replace"])
    paras = [p for p in re.split(r"\n\s*\n", text) if p.strip()]
    found = sum(" ".join(words(p)) in final_words for p in paras)
    ok &= same_hash and found == len(paras)
    rows.append(f"| {bid} | {'yes' if same_hash else 'NO'} | {found}/{len(paras)} |")

report = ["# Verification report", "",
          f"Result: {'PASS' if ok else 'FAIL'}", f"Humanized blocks: {len(manifest)}",
          f"Fact patches applied: {len(patches)}", "",
          "| Block | Hash unchanged | Paragraphs found verbatim |", "|---|---|---|", *rows]
(ROOT / "VERIFY_REPORT.md").write_text("\n".join(report) + "\n")
print("\n".join(report[:5]))
sys.exit(0 if ok else 1)
```

If a paragraph is "not found" only because the PDF split a word oddly or a ligature (ﬁ, ﬂ) came out of the PDF, add that normalisation to `words()`. Do not touch the text to make the check pass.

---

## Appendix H. scripts/check_refs.py

```python
"""Check every reference against Crossref (and arXiv for arXiv ids). It only reports; fix references by hand.
Usage: python scripts/check_refs.py penned_work/draft/references.json
references.json: [{"n": 1, "title": "...", "authors": "...", "year": 2020, "venue": "...", "doi": "..." or null}, ...]"""
import difflib, json, re, sys, time, urllib.parse
import requests

UA = {"User-Agent": "penned-playbook/1.1 (reference check)"}

def similar(a, b):
    norm = lambda s: re.sub(r"[^a-z0-9 ]", "", (s or "").lower())
    return difflib.SequenceMatcher(None, norm(a), norm(b)).ratio()

def crossref_doi(doi):
    r = requests.get(f"https://api.crossref.org/works/{urllib.parse.quote(doi)}", headers=UA, timeout=30)
    return r.json()["message"] if r.status_code == 200 else None

def crossref_search(ref):
    q = f"{ref['title']} {ref.get('authors', '')} {ref.get('year', '')}"
    r = requests.get("https://api.crossref.org/works", params={"query.bibliographic": q, "rows": 3}, headers=UA, timeout=30)
    return r.json()["message"]["items"] if r.status_code == 200 else []

def arxiv(aid):
    r = requests.get("https://export.arxiv.org/api/query", params={"id_list": aid}, timeout=30)
    m = re.search(r"<entry>.*?<title>(.*?)</title>", r.text, re.S)
    return re.sub(r"\s+", " ", m.group(1)).strip() if m else None

bad = 0
for ref in json.load(open(sys.argv[1])):
    title, status = ref["title"], "UNVERIFIED"
    aid = re.search(r"(\d{4}\.\d{4,5})", json.dumps(ref)) if "arxiv" in json.dumps(ref).lower() else None
    if ref.get("doi"):
        m = crossref_doi(ref["doi"])
        if m and similar(title, (m.get("title") or [""])[0]) > 0.85:
            venue = (m.get("container-title") or [""])[0]
            year = (m.get("issued", {}).get("date-parts") or [[None]])[0][0]
            status = "OK" if str(year) == str(ref.get("year")) else f"YEAR? Crossref says {year}"
            if venue and ref.get("venue") and similar(venue, ref["venue"]) < 0.6:
                status += f"; VENUE? Crossref says '{venue}'"
        elif m:
            status = f"DOI belongs to another paper: '{(m.get('title') or [''])[0]}'"
        else:
            status = "DOI not found"
    elif aid:
        t = arxiv(aid.group(1))
        status = "OK (arXiv)" if t and similar(title, t) > 0.85 else f"arXiv id is a different paper: '{t}'" if t else "arXiv id not found"
    else:
        hits = crossref_search(ref)
        best = max(hits, key=lambda h: similar(title, (h.get("title") or [""])[0]), default=None)
        if best and similar(title, (best.get("title") or [""])[0]) > 0.9:
            status = f"FOUND, add DOI {best.get('DOI')}"
    bad += not status.startswith(("OK", "FOUND"))
    print(f"[{ref['n']}] {status}: {title[:80]}")
    time.sleep(0.5)
print(f"\n{bad} reference(s) need attention. Books, reports and standards are often missing from Crossref: check those by hand.")
sys.exit(1 if bad else 0)
```

---

## Appendix I. scripts/trace_numbers.py

```python
"""List numbers that appear in the paper but nowhere in the user's material (likely invented or mistyped).
Usage: python scripts/trace_numbers.py <final.pdf|.tex|.txt|draft folder> <user material folder>"""
import pathlib, re, subprocess, sys

NUM = re.compile(r"(?<![\w.])[-+−]?\d[\d,]*(?:\.\d+)?(?![\w])")

def text_of(path):
    p = pathlib.Path(path)
    if p.is_dir():
        return "\n".join(text_of(f) for f in sorted(p.rglob("*")) if f.is_file())
    if p.suffix == ".pdf":
        return subprocess.run(["pdftotext", str(p), "-"], capture_output=True, text=True).stdout
    if p.suffix == ".docx":
        import docx
        return "\n".join(x.text for x in docx.Document(str(p)).paragraphs)
    if p.suffix in (".txt", ".md", ".tex", ".csv", ".json", ".bib", ".py", ".ipynb", ".log", ".tsv"):
        return p.read_text(errors="ignore")
    return ""

def nums(t):
    out = set()
    for n in NUM.findall(t):
        n = n.replace(",", "").replace("−", "-").lstrip("+")
        out.add(n)
        if "." in n:
            out.add(n.rstrip("0").rstrip("."))   # 72.30 and 72.3 are the same value
    return out

paper, source = text_of(sys.argv[1]), text_of(sys.argv[2])
cut = max(paper.rfind("\nReferences"), paper.rfind("\\begin{thebibliography}"), paper.rfind("\\bibliography{"))
paper = paper[:cut] if cut > 0 else paper          # the reference list is checked by check_refs.py instead
paper = re.sub(r"https?://\S+|doi:\S+|\b10\.\d{4,9}/\S+", " ", paper)
known = nums(source)
missing = sorted(n for n in nums(paper) if n not in known and not re.fullmatch(r"-?\d", n) and not re.fullmatch(r"(19|20)\d\d", n))
print(f"{len(missing)} number(s) in the paper not found in the material:")
for n in missing:
    i = paper.find(n)
    print(f"  {n:>10}  ...{paper[max(0, i - 60):i + 40].replace(chr(10), ' ')}...")
```

Numbers you computed from the material (a difference, an average) are fine, but write how you got each one in PROGRESS.md. Anything else on the list is a fact error until shown otherwise.

---

## Appendix J. Quick reference

- API base: https://penned-ai.vercel.app
- Humanize: `POST /v1/jobs`, header `X-API-Key: <key>`, body `{"text": "...", "category": "academic"}`, 40 to 1,200 words, returns `{"job": ...}`. Then `GET /v1/jobs/<job>` until `status` is `done`: `output`, `ai_score_before`, `ai_score_after`, `meaning_kept`, `numbers_kept`, `review`, `words`, `seconds`.
- Detect: `POST /v1/detect`, same header, body `{"text": "..."}`. Response: `ai_score`, `detectors`, `sentences`.
- Limits: 50 API calls a day per account (humanize and detect counted separately), reset at midnight UTC.
- API keys: https://penned-ai.vercel.app/app/keys. Docs: https://penned-ai.vercel.app/developers
