← All posts

A document that reads itself before you send it

The red team stops being a habit the writer has to remember. export_document_pdf now refuses to print a client-facing document with a blocking finding on its current version — and reads the document itself if nobody has.

Vocion Teamvocion-v2.123.1

Three things go wrong in a proposal that no renderer will ever catch.

It promises an outcome — a projected saving, a return figure, a percentage lift — when what you actually commit to is a capability and a measurement. It quotes a number nobody can source, because the model needed one and the room did not have one. Or it contradicts itself: a price on sheet 7 that does not match the scope on sheet 6, a roadmap that reads like a commitment, an ownership question the buyer's legal team will ask and the document never answers.

Every one of those survives a clean render-verify receipt. Every sheet fits, no footer moved, the PDF has as many pages as there are sheets, and the document is still not safe to send.

Yesterday's answer was red_team_document: a tool that reads the whole thing as the sceptical operations lead who has to carry it into an internal business case, and returns findings by sheet. It works. The problem was that it only ran when the agent remembered to, because the skill asked for it in prose — and prose is the weakest lever there is.

Today it is a gate.

The export enforces it

export_document_pdf is the moment a document stops being a draft. So that is where the check lives. Before it prints a client-facing document it asks one question of the row: has this version been read as the buyer, and did it come back clean?

The answer is on the artifact, not in a model call. The review is written onto the version it read, at spec.redTeam, exactly the way the render-verify verdict already was. Freshness is presence, not arithmetic: any edit to the words drops the receipt and the document is unread again. A re-verify keeps it, because re-verifying does not change what a sheet says.

Five states, and only one of them stops anything:

The receipt

When it clears, the export receipt carries the read as a line a person can check:

Read as the buyer at v12 on 2026-09-19 (claude-sonnet-4-6, 9 sheets):
no blocking findings (3 to fix · 2 to consider).

When it does not, there is no PDF and the refusal is the work list — every blocking finding with its sheet, what a buyer would actually read, and the edit that answers it, so the writer can act without another tool call:

NOT exported. "claude-sonnet-4-6" read this as the sceptical buyer at v12
and found 1 blocking finding. This document is not sent until it is answered.

1. sheet 9 · commercial clarity: The investment sheet gives a monthly figure
   and a four-month term but never states the total, and the term's end is
   described two different ways on sheets 6 and 9. A buyer taking this to
   finance cannot answer "what does it cost and when does it stop?"
   → State the total alongside the monthly figure, and make sheet 6 use
   sheet 9's wording for what happens at the end of the term.

Fix each one with edit_document on the named sheet, then run
red_team_document again. Export once it comes back with no blocks — or say
to the person which finding you are overriding and why.

Findings are graded three ways, and only the first one stops a send: block (must change before this goes out — an unsourced number, a promised outcome, a contradiction in price or scope), fix (a buyer would notice and it costs trust), consider (a judgement call worth a look).

Beside the verify chip — on the document pane, the room page and the Proposals board — the state reads as one line: not read as the buyer, read as the buyer · clean, read as the buyer · 3 to fix, or, in amber, read as the buyer · 2 blocking.

The Proposals app: an engagement at Proposal stage whose latest document reads "5 sheets · verified" on one line and "not read as the buyer" on the next

Which documents are gated is yours

Core does not hold a list of what counts as a client document. Your workspace does:

# workspace.yaml
defaults:
  clientFacingPlaybooks: [proposal, scope, partnership-update]

Those three are the default when a workspace says nothing. email-copy and work-sample are deliberately out: they are short, cheap to re-send, and gating them would put a model call in front of every draft email. Tags match case- and separator-insensitively, because a playbook tag is free text an agent types.

One distinction is load-bearing. No list means "this workspace never said", and falls back to the defaults. An empty list — clientFacingPlaybooks: [] — means "this workspace gates nothing", and is honoured. The migration that added the column left it nullable with no default precisely so those two could not collapse into each other and quietly turn the gate off for every existing workspace.

Two things we are being honest about

The gate's own read is weaker than the writer's. If nobody ran the red team, the export runs it on core's generic rubric only — without the house rules your proposal-document skill carries, and treating every figure as unsourced unless the sheet marks its own source. The gate is a floor, not a ceiling. The writer should still run it itself, because the rubric is the part the gate cannot supply.

Fix-and-re-run does not converge on its own. This is a model reading prose, and two consecutive passes over the same document will not always name the same set of findings. The temperature is pinned at zero, the findings are salvaged individually so one over-long finding cannot lose the other eleven, and blocks are sorted first so a cap can never drop a blocker — but none of that makes the pass deterministic. In practice you can fix every block, run again, and meet a new one you had not seen.

That is tolerable for a gate whose only job is to stop the worst thing going out, and it is not good enough as a workflow. The design question we have not answered yet is what to do with a finding that recurs: a block that comes back on a second pass, or on a second document, is not noise — it is a rule the writer should have started with. Blocking on recurrence rather than on any single pass is where this goes next, and it is the same shape as the proposal-feedback learning step already in the plugin: would this apply to the next client's proposal? Then it is a rule, not an edit.

Until then: a client document with a blocking finding does not become a PDF. That is a smaller claim than "your proposals are now correct", and it is one you can check.

Read the whole thing in Proposals.