Full Stack Engineering

Redpen

An AI-powered Microsoft Word add-in that brings drafting, review, rewriting, summarization, and templating directly into a lawyer's document, built on Office.js, Node/Express, and the Claude API. Its centerpiece is an independent suggested-changes engine: every AI edit is proposed, reviewed, and explicitly accepted or rejected before it ever touches the document.

Every mark a lawyer makes on paper starts as a strike-through and a note in the margin. Redpen rebuilds that instinct as a controlled AI review layer.

Role Solo, Full Stack
Context Independent prototype, legal tech
Timeline 2026
Stack Node.js · Express · Office.js · Claude API

AI that lives inside the document, not next to it.

Legal professionals spend much of their time moving between documents, precedents, internal standards, templates, and drafting tools. A general-purpose AI chat interface can generate legal text, but it introduces friction when the real work is happening inside Word: copy the clause out, explain its context, copy the answer back, find the right paragraph again, manually manage the revision.

Redpen is a Word task-pane add-in designed around how lawyers actually work: select a clause, ask the AI to analyze or modify it, inspect the result, navigate back to the relevant language, and decide what should ultimately change. It evolved into a multi-feature document workspace with structured AI outputs, document-aware actions, reusable legal templates, playbook-assisted review, and an independent lifecycle for controlling every AI-generated edit.

5 Word-integrated workflows: Summarize, Rewrite, Simplify, Review, Templates
7 backend API endpoints, each with its own prompt, schema, and validation logic
6-stage suggested-change lifecycle from Review through History
1 shared editing seam every feature's document mutation routes through

A thin, deliberate proxy between Word and the model.

The add-in runs as a Word task pane. The frontend talks to the live document through Office.js and the Word JavaScript API: reading the current selection, inspecting the whole document, searching for specific clauses, inserting paragraphs, replacing ranges, and moving the Word selection to relevant content. AI work is separated into backend endpoints, one per feature, each with its own prompt and response schema, so the frontend renders feature-specific interfaces instead of dumping every AI response into a chat bubble.

Word Desktoptask pane, loads manifest.xml
→
taskpane.jsOffice.js, reads/writes the live document via Word.run()
→
server.jsExpress, holds the API key, builds feature-specific prompts
→
Claude APIstructured response, rendered into feature-specific UI

Every user action is scoped to one of seven endpoints, so a bug in one workflow can't leak into another:

/api/summarizestructured summary, key points, risks
/api/rewritepreset or free-text drafting instructions
/api/simplifyaudience- and tone-aware explanation
/api/reviewstructured findings, optionally playbook-aware
/api/suggest-revisionturns a finding into a proposed edit
/api/detect-placeholdersregex + LLM template field detection
/api/autofill-templatefills template fields from document context

Five tools, five different jobs.

Each feature is scoped narrowly on purpose. Summarize never rewrites. Rewrite never explains. Simplify is structurally prevented from producing clause-shaped output. Every one is selection-aware: if text is selected in Word, only that content is sent; if nothing is selected, the add-in falls back to the full document automatically.

Summarize

Returns a structured result rather than one block of prose: a plain-English Summary, extracted Key Points, and any Potential Risks identified in the text, each rendered as its own section so it can be scanned rather than read. On insertion, the add-in first tries to relocate the original summarized selection as an anchor, and falls back to the current selection if that text can no longer be found.

Short / Medium / Detailed Selection-aware Structured JSON output

Rewrite

Presets like More Concise, More Formal, Plain English, Better Flow, Buyer-Friendly, and Supplier-Friendly remain fully editable before sending, so a preset acts as a starting point rather than a fixed command. The result can be copied, used to replace the selection, or inserted below the original for side-by-side comparison. The add-in retains a link between the generated revision and the clause that produced it, even after the user's cursor moves elsewhere.

6 quick presets Free-text instructions Replace or insert below

Simplify

"Simple" means something different depending on who's reading. Users choose a target audience, client, executive, sales, or a custom audience typed in directly, plus a tone (Professional, Conversational, Very Simple). Changing either control changes the actual instruction sent to the model, not just a UI label. Simplify is structurally prevented from reading like a redraft: it explains, it doesn't produce new legal language.

Custom audience field Adjustable tone Never clause-shaped

Review

Analyzes text against configurable review types, general legal risk, missing clauses, ambiguities, inconsistencies, business risk, and optionally a named firm playbook. Returns structured findings with severity, explanation, and a recommendation. Review never edits anything directly; suggesting a revision is a separate, explicit step per finding, covered in section 05.

6 review types Playbook-aware Jump-to-clause navigation

Templates

Two flows: turning a live document into a reusable template with {{PLACEHOLDER}} tokens, and filling a saved template back in. Opening Fill Template first scans the current document for existing tokens; if the document already contains placeholders, it becomes the source of truth immediately, no need to re-select the same template from a list. Covered in full in section 06.

Two-pass detection AI autofill Incremental refill

Findings, not a verdict.

Instead of one paragraph saying whether a contract "looks fine," Review returns structured findings, each with a severity, title, explanation, recommendation, source, and the exact clause text that triggered it. Findings render as expandable cards rather than chat text, so a lawyer can triage by severity before reading any explanation in full.

High Playbook Requirement

Limitation of liability cap is uncapped for indirect damages

The current clause excludes indirect and consequential damages but does not cap direct damages, which conflicts with the firm's standard liability position. Playbook rule LP-04 requires a direct-damages cap tied to fees paid in the preceding 12 months.

Medium Best Practice

Notice period for termination is ambiguous

"Reasonable notice" is used without a defined number of days, which invites dispute over what constitutes compliance. Most comparable agreements specify 30 or 60 days explicitly.

Low Best Practice

Governing law clause omits venue

Governing law is specified, but no exclusive venue or forum is named for disputes, which can complicate enforcement.

When playbook review is enabled, a finding can carry the specific internal rule that triggered it, shown alongside the requirement, how the current document compares, and the result of that comparison. This lets the AI explain why something was flagged rather than simply asserting that a clause is a problem, and it means a suggested revision can reuse the same playbook rule as context for the replacement language.

Jump to Clause, with fallback strategies

Each finding retains the underlying clause text that caused it. Selecting Jump to Clause searches the live document for that text and moves the Word selection there directly. Since generated or normalized text doesn't always exactly match Word's underlying representation, the search runs multiple strategies, exact match, normalized whitespace, and shorter candidate snippets, and if none succeed, the interface disables navigation rather than silently jumping somewhere wrong.

Best Practice vs. Playbook Requirement

The interface distinguishes findings sourced from general legal drafting guidance versus a firm's own internal standard, so a reviewer knows immediately whether a flag reflects broad convention or a rule the firm has specifically decided matters.

Why not just use Track Changes?

This was the hardest, and most rewritten, problem in the project. The original model was simple: an AI response would replace text in the document immediately. That worked in the happy path, but any interruption, a slow response, the user editing nearby text, closing the task pane mid-generation, could leave the document in a state nobody asked for. For a legal tool, a corrupted document is worse than a missing feature.

The redesign introduces a real approval boundary between two operations that used to be one: "the AI thinks this should change" is no longer the same event as "the document changes."

1

Review

A finding is surfaced with severity, explanation, and recommendation. Nothing in the document has changed yet.

2

Suggest Revision

An explicit user action sends the clause, issue, explanation, and any playbook context to a dedicated revision endpoint.

3

Pending Change

The response becomes a SuggestedChange object: original text, proposed text, reasoning, source, and document location, held in a queue rather than applied.

4

Accept / Reject

The user reviews the proposed edit side by side with the original and makes the call. Only Accept touches the document.

5

Document Update

On accept, the original clause is relocated in the live document and replaced. If it can no longer be found, the change is not marked accepted.

6

History

Nothing is deleted from the queue on reject. Every resolved change, accepted or rejected, moves to a timestamped history rather than disappearing.

Try it: an example finding turned into a suggested change

Limitation of liability, direct damages cap Pending
−In no event shall either party's liability exceed the amounts actually paid, except for indirect or consequential damages, which are excluded entirely.
+In no event shall either party's total liability under this Agreement exceed the fees paid in the twelve (12) months preceding the claim, excluding indirect or consequential damages.
Why this change
Adds a direct-damages cap tied to trailing fees, closing the gap the original clause left open.
Source
Playbook rule LP-04, Limitation of Liability

This is a static illustration of the actual accept/reject/history flow described above, not a live model call.

The right abstraction sometimes only becomes visible on the second rewrite. Moving the redline concept out of the document and into a UI-only state machine became the foundation for every feature built after it.

Cheap detection first, AI only where it earns its keep.

Turning a document into a template means finding every span of text that's specific to this document and should become a fill-in-the-blank field. Detection runs in two passes: a regex pass that catches deterministic patterns instantly and for free, emails, phone numbers, dollar amounts, dates, city and state lines, and an LLM pass that catches what regex structurally can't, person names, company names, job titles, reference numbers. The two are merged, deduplicated, and auto-numbered so the placeholder list reads top to bottom the way the document does.

server.js, two-pass detection with graceful degradation
async function detectPlaceholders(text) {
    const regexMatches = runRegexDetectors(text);
    let aiMatches = [];
    try {
        aiMatches = await runAiDetector(text);
    } catch (err) {
        // the AI pass failing shouldn't break the whole request,
        // regex matches are still useful on their own
        console.error("AI placeholder detection failed:", err.message);
    }
    return finalizePlaceholders([...regexMatches, ...aiMatches], text);
}

Document first, dropdown second

Opening Fill Template scans the current document for {{PLACEHOLDER}} tokens first. If they're already there, Fill Mode opens immediately using the open document as the source of truth. Only if none are found does the user pick from a saved template list, and filling a blank document versus one with existing content requires explicit confirmation before anything is overwritten.

Autofill never overwrites a manual value

Template fields can be populated from surrounding document context via /api/autofill-template. The merge order is strict: existing manual value, then AI suggestion, then empty. The interface visibly distinguishes AI-suggested values from fields that still need manual input, and every suggestion stays editable.

Incremental, not destructive, refills

The add-in maintains a mapping between each placeholder and its current value in Word. Clicking Fill again only searches for and replaces fields that actually changed since the last fill, so a user can fix one field and reapply without rebuilding or overwriting the whole document. This state persists in Word document settings, tied to the file itself.

Readable labels, real tokens underneath

Raw tokens like {{SUPPLIER_COMPANY_NAME}} are converted into readable form fields such as "Supplier Company Name," grouped into sections like Parties, Contact, Commercial, and Dates, while the original token stays visible for clarity about what's actually being substituted.

One editing seam, not five.

As the prototype grew, multiple features needed to manipulate Word ranges. Rather than letting each one implement document editing independently, every document mutation now passes through the same applySuggestedChange() function: locate the change, relocate the original clause in the current document, verify the source text still exists, apply the replacement, select the resulting text, and update the stored document location so the change can still be found later.

taskpane.js, the single editing seam
async function applySuggestedChange(changeId) {
    const change = findChange(changeId);
    const located = await locateOriginalClause(change.documentLocation);

    if (!located) {
        // source text no longer exists, refuse to mark this accepted
        return { ok: false, reason: "Original clause could not be located." };
    }

    await Word.run(async (context) => {
        located.range.insertText(change.suggestedText, Word.InsertLocation.replace);
        located.range.select();
        await context.sync();
    });

    // only marked accepted after the document operation succeeds
    change.status = 'accepted';
    moveToHistory(change);
    return { ok: true };
}

Acceptance is earned, not assumed

If the original clause has been manually edited or deleted since the suggestion was generated, the system refuses to mark it accepted and reports that the source text couldn't be found, instead of silently applying an edit to the wrong place.

A seam built for what comes next

Centralizing every document mutation behind one function means the underlying editing implementation can later be swapped for a true redline or tracked-changes engine without rewriting the Review, Templates, or Changes workflows built on top of it.

The document is live, not a textarea.

The hardest problems in this project weren't calling an LLM. They were deciding how generated output maps back onto a document that keeps changing underneath it.

Working with Word, not plain text

Selections, ranges, document searches, paragraph insertion, replacement, and cursor movement all had to account for a document that can change between generation and application, not a static string.

Maintaining document location

AI suggestions can stay visible long after the user's cursor has moved elsewhere. Results retain the source text that generated them and attempt to relocate it when an action is later performed, rather than trusting the current selection.

Making AI output deterministic enough for UI

Raw model responses are difficult to build reliable interfaces around. Summarize and Review both structure output into predictable fields so the frontend can render summaries, severities, recommendations, and playbook references as real product components, not chat text.

Separating generation from mutation

The single most important architectural decision in the project: "the AI thinks this should change" is not the same event as "change the document." The suggested-changes queue is the approval boundary between them.

Preserving user control

AI-generated values and revisions remain editable or rejectable everywhere in the product. Autofill preserves manual entries, findings require an explicit revision request, and revisions require explicit approval before touching the document.

What it's built with.

The task-pane UI, the Word Object Model integration, the Express proxy, every prompt, and the suggested-changes state machine were all written directly, with the Claude API handling language understanding and generation.

Node.js Express Office.js Claude API JavaScript HTML / CSS OOXML / Word Object Model
Current prototype scope

This is an independently developed prototype, not a production deployment. Summarize, Rewrite, Simplify, Review, Templates, and the suggested-changes workflow all have functional Word integration. A few actions visible in the interface, proofreading, translation, anonymization, and standalone clause drafting, currently use prototype or mock behavior rather than complete backend workflows, and a full persistent redline visualization inside the document is an intended next step rather than something already built.

What I took away.

🔒

Silent failures are worse than crashes

This is especially true in high-stakes tools. A parsing failure that quietly returns an empty result is more dangerous than one that visibly errors, because the user has no signal anything went wrong. It shaped how every endpoint handles malformed model output.

🎯

Narrow scope made the AI trustworthy

Review never rewrites. Rewrite never explains. Simplify never redrafts. Enforcing that boundary in the prompt and the response schema made each tool's output predictable enough to actually rely on, instead of double-checking whether "summarize" secretly changed a meaning somewhere.

🏗️

The right abstraction can take a rewrite to find

The first version of document editing, mutate immediately, seemed reasonable until it broke under real interruption. The fix wasn't a patch. It was moving generation and mutation apart entirely, which became the foundation everything else was built on.

Next project

AI Harness

View case study