Document Generation
Opbox generates DOCX documents from template packs using a patch-not-reconstruct model: the original DOCX is preserved byte-for-byte and the AI applies surgical edits to bookmarked anchors. This avoids round-tripping through any lossy intermediate representation.
Three generation modes:
| Mode | What the AI fills | Source |
|---|---|---|
| TEMPLATE_FILL | Variables (names, dates, percentages) | Matter payload + linked records |
| ASSISTED_DRAFT | Prose sections (clauses, narrative) into named bookmarks | AI prompt + matter context |
| FULL_AUTHOR | The whole document from scratch | AI prompt + matter context (Phase 0+) |
All three modes share the same coverage gate, the same patcher, and the same tamper-evident snapshot.
Mental Model
Template DOCX + Matter Payload + AI prompt
│
├── Detection: identify variables, sections, tables ────┐
│ │
├── Coverage Gate (PASS / WARN / FAIL) ─────────────────┤
│ │
├── Templater Agent ─────────────────────────────────► Patch Operations
│ (Sonnet semantic mapping, Zod-validated) │
│ │
├── Patcher (replaceText / insertBookmarkAndClear / │
│ replaceParagraphRange / replaceTable) ◄───┘
│
└── Generated DOCX + Snapshot (checksum, payload, mode)
Bookmark Naming Convention
Templates declare anchors using bookmarks with a controlled prefix:
| Prefix | Purpose |
|---|---|
opbox_section_<name> | Prose section anchor. The patcher replaces the paragraph range between bookmark start and end with AI-rendered Tiptap content. |
opbox_table_<name> | Table anchor. The patcher replaces the entire <w:tbl> element. |
opbox_var_<name> | Variable anchor. The patcher inserts the resolved value and clears the placeholder text. |
The BookmarksPanel in the document workbench lists all bookmarks in a DOCX with kind classification, click-to-scroll navigation, and per-row delete.
Coverage Gate
A pure-function coverage gate classifies generation readiness before any writes occur.
| Result | Meaning | Generation behaviour |
|---|---|---|
| PASS | Every required variable resolves to a non-empty value | Generate. |
| WARN | All required variables resolve but optional ones are missing | Generate, mark missing in snapshot. |
| FAIL | One or more required variables are unresolved | Reject 422. |
/api/documents/packs/[packId]/generate and /preview both call the gate before doing any writes. FAIL short-circuits to 422 with a structured error listing the missing variables.
nullGetter emits [MISSING: name] markers - no silent empties. Means a downstream reviewer can grep the rendered doc to find unresolved spots even when the gate says WARN.
Patcher Operations
The DOCX patcher implements a small set of surgical operations against the source XML:
| Op | Purpose |
|---|---|
insertBookmarkAndClear | Insert content at a bookmark and clear any placeholder text in the bookmark range. Used by opbox_var_*. |
insertTableBookmark | Insert a <w:tbl> at a bookmark. Used by opbox_table_*. |
replaceParagraphRange | Replace the paragraph range between two bookmark markers. Used by opbox_section_*. |
replaceTable | Replace an entire existing <w:tbl> element. |
replaceText | Single-run text replacement (TEMPLATE_FILL fast path). |
replaceParagraphText | Cross-run text replacement with anchor-run rPr clone. Used when a placeholder spans multiple <w:r> elements. |
removeBookmark | Strip both <w:bookmarkStart> and <w:bookmarkEnd> markers without touching content. |
resolveBookmarkRange | Helper that finds bookmark start/end markers and returns the paragraph range. |
A pre-patch normalisation step coalesces split placeholders so a {name} that Word has split across two runs (because of formatting) collapses back to a single replaceable run.
Templater Agent
The templater is a Sonnet-driven semantic mapper. It receives:
- A list of detected placeholders (
{full_name},{registered_address_line_1}, ...) - The matter payload (already normalised by
matter_get_payload) - The available variable registry from
pack_get_template_variables
And returns (via forced tool-call, schema-validated):
- A mapping of placeholder -> resolved value
- Optional prose section content (Tiptap JSON)
- Optional dynamic-table rows
Errors surface with a kind discriminator: api_error / no_tool_use / invalid_output / timeout. An empty API key is rejected upfront.
Tiptap to OOXML
A minimum-viable Tiptap-to-OOXML converter supports ASSISTED_DRAFT mode:
- Paragraphs and headings
- Marks (bold, italic, underline)
- Alignment
- Inherited paragraph properties
Deferred to later phases: lists, tables, hyperlinks, mentions.
Generation Snapshot
Every generated document writes a tamper-evident snapshot with:
| Field | Purpose |
|---|---|
snapshotChecksum | SHA-256 of snapshotPayload. Changes if anything is rewritten. |
snapshotPayload | The exact inputs that produced the doc (template, payload, mappings, prose, tables). |
templateChecksum | The template's checksum at generation time. Detects whether the template has since changed. |
coverageResult | PASS / WARN / FAIL. |
missingVariables | List of unresolved required variables (empty on PASS, populated on WARN). |
generationMode | TEMPLATE_FILL / ASSISTED_DRAFT / FULL_AUTHOR. |
generationStatus | Derived from coverage + run outcome. |
Snapshots make downstream audits trivial: every generated doc can be regenerated bit-identical from its snapshot, and any divergence between a snapshot and the rendered doc proves tampering.
Cloud-Fire Dispatch
The doc-gen runtime is "Claude Code connected to the Opbox MCP." Two ways to start it:
- Pull-only - any local Claude Code session running with an MCP key can drain the queue.
- Cloud-fire - Opbox POSTs a wake-up ping to a hosted agent provider on step entry; the hosted Claude Code claims the task autonomously.
Cloud-fire is configured at Settings > AI > Doc Generation Agent:
| Field | Purpose |
|---|---|
| Provider | claude-routine (Anthropic Claude Code Routine) or openai-frontier (OpenAI hosted agent) |
| Fire URL | The provider's per-routine POST endpoint |
| Bearer token | Auth for the fire URL. Encrypted at rest. |
| Routine owner | The workspace member whose identity the hosted agent acts as. Use a service account in production. |
| Daily cap | Provider-imposed ceiling on fires per day (informational). |
Test with the Test fire button. Disable globally with the Doc-generation agent enabled kill-switch; when off, steps that require generation fall back to NEEDS_MANUAL_GENERATION.
MCP Tool Surface for Doc-Gen
| Tool | Purpose |
|---|---|
matter_get_payload | Normalised matter context (entity, stakeholders, line items, properties). |
matter_get_step | Step config + current data. |
matter_validate_before_generate | Run coverage gate without generating. |
pack_get_template_variables | Variable registry from the pack. |
pack_get_prose_sections | Section bookmarks + descriptions. |
pack_get_dynamic_tables | Dynamic-table bookmarks. |
step_get_data | Form-step data already collected. |
data_source_list / search / get_item | Pluggable data-source registry (addons populate). |
Plus the Agent Queue tools (agent_queue_*) for claim/lease/release.
These tools are registered under the doc-gen MCP category. Queue mutations gate at autonomy L1, reads at L0.
API
| Endpoint | Purpose |
|---|---|
POST /api/documents/packs/[packId]/generate | Run generation with the matter payload. Coverage FAIL returns 422. |
POST /api/documents/packs/[packId]/preview | Preview without writing the generated doc. |
GET /api/documents/generated/[id] | Generated doc metadata + snapshot. |
GET /api/documents/generated/[id]/download | Stream the rendered DOCX (or zipped multi-file). |
GET /api/documents/doc/[id]/bookmarks | List bookmarks in a DOCX. |
DELETE /api/documents/doc/[id]/bookmarks/[name] | Remove a bookmark (markers only - content untouched). |
GET /api/documents/doc/[id]/styles | Word paragraph styles for the Format > Paragraph style menu. |
See Also
- Agent Worker - the queue model that doc-gen runs on top of.
- Agent Bridge - the Claude Code subprocess that runs the loop.
- Autonomy Levels - what L0/L1 unlock for doc-gen.