Fixing Docs
Overview
A validated, current approval set is the only authority, and the applier is the only writer. One door lands every record type — drift and bloat alike — because after minting the flow is identical; the record's finding code is internal routing, not a different skill.
You never edit a document under audit. Not with Edit, not with Write, not with
sed, not "just this one line". Every byte that lands is written by
python3 -m doclifecycle apply-plan, from an edit plan bound to an approval set the
engine validated against both the report and the repository.
Violating the letter of the flow is violating the spirit of the flow. The point is not ceremony: a diff produced any other way carries no proof of what authorized it, so a reviewer cannot tell an approved remedy from your opinion.
Contract (read it, do not restate it): ${CLAUDE_PLUGIN_ROOT}/engine/README.md,
sections Approval sets and The applier. Run engine commands with the plugin's
engine directory on PYTHONPATH.
REQUIRED SUB-SKILL: use writing-docs for any replacement text you must author yourself (a paragraph, a merged remainder) rather than place — the report's proposal already meets that bar and is placed byte-verbatim.
The flow — four steps, in order, every time
1. Mint the approval set
The input is one engine report — drift-audit or bloat-audit output — plus the
record digests a person selected from it. That selection is the semantic approval,
and minting is what turns it into the artifact; the names alone are not it.
python3 -m doclifecycle mint-approval --report "${TMPDIR:-/tmp}/report.json" --repo . \
--record <record digest> --minter <who approved> --out /tmp/approval.json
--record takes the record's digest, not its display id — the id is a label the
report can renumber. Repeat it once per approved record. Write the artifact
outside the work tree or to a git-ignored path; the engine refuses a tracked or
would-be-tracked path, because a git add -A in the change it authorizes would
commit the authority next to the diff.
That same placement rule covers every audit artifact you carry into this flow, not just
the approval set — the report, the verdicts/plan the detecting skill produced, and the
edit plan you author in step 2. Step 3's confinement check (below) compares the working
tree against the approval set's paths, so a drift-report.json or verdicts.json left
sitting in the tree reads as an unaccounted change and the run refuses before it applies
anything. detecting-doc-drift and detecting-doc-bloat write their artifacts to
${TMPDIR:-/tmp}/ for exactly this reason; keep them there rather than moving them into
the repo to "review before minting."
Minting refuses before it mints; what it refuses, and in what order, is the engine README's Approval sets section. A refusal is the answer, not an obstacle: report it and stop.
2. Author the edit plan
This is the one artifact you author, and its shape is not yours to remember. The closed
operation vocabulary, each operation's exact field set, the preimage and postimage
rules, and which operations each finding code's remedy is made of (RECORD_REMEDIES)
are the engine README's The applier section, over
${CLAUDE_PLUGIN_ROOT}/engine/doclifecycle/applier.py. Read each operation's field set
there and build the operation from it — never from memory, an older plan, or an example.
Three rules are yours rather than the engine's, and each one is a way an authorized run still lands an unauthorized diff.
The remedy belongs to the record, not to you. The finding code decides which operations
its plan may carry; a plan that picks the operation puts the choice back with the model, and
the engine refuses it (plan-operation-not-record-remedy). When the fix seems to need an
operation the code does not authorize, that is something to surface for the next audit, not
a plan to widen.
Widen nothing. A positioned operation may name only the record's own document, and must lie inside that record's approved assertion units. An adjacent passage the approval did not cover is a separate record's business even when the two sit one paragraph apart, and even when the report drafted that neighbour's text.
The text inside the operation is the report's, not yours. A STALE record's fix
and a CONDENSE record's proposal are complete replacement text drafted to the
writing-docs bar — place them byte-verbatim and stop at their final character. The approved
units bound where you may write; nothing bounds what, so authoring your own sentence
inside an approved span produces a diff the applier certifies and no reviewer approved.
A fix may itself contain LF when its assertion unit was soft-wrapped; that LF, the list
marker, and the continuation indentation are part of the approved replacement, so copy the
whole string in and never collapse or re-wrap it while planning. Compose text only where
the record supplies none (a merged remainder, a distillation's residue), and route it
through writing-docs.
3. Run the applier
python3 -m doclifecycle apply-plan --repo . --plan "${TMPDIR:-/tmp}/plan.json" \
--approval /tmp/approval.json --report "${TMPDIR:-/tmp}/report.json" \
[--audit-config-digest <sha256>]
--report is required: without it the approval set's authority check is a function of
public repository state, so a selection nobody minted would validate. The working tree
must be clean before you run it — the applier applies onto the committed baseline, so
an unrelated edit sitting in the tree refuses the run: outside the approval's scope as
apply-working-tree-not-confined, inside it as apply-working-tree-not-clean. Commit or
discard first.
Re-running an interrupted lane is safe: the no-op verdict is derived from the bytes on disk, never declared by the plan. The exit codes and every refusal code the run can return are the engine README's — The applier, and Approval sets for the authority check it runs first.
already_applied: true on a run you have not made before is a tripwire, not a
success: the bytes were already on disk, which means something other than the applier
put them there. Say so rather than presenting the diff as this run's work.
4. Present the working-tree diff for change approval
The applier never stages and never commits. Change approval — a person accepting the
produced diff — is the only thing that lands anything, so the run ends by showing the
working-tree diff, the applied operations with their records, and the approval trailers
(python3 -m doclifecycle render-approval --approval /tmp/approval.json --trailers) for
the commit message or PR body. You do not commit, push, or open a PR unless the person
asks for it as a separate step.
The approval set itself never enters the repository. Its digest and rendered summary do.
Refusals — before any work, and non-negotiable
| Situation | What you do |
|---|---|
| No approval-set file exists | Stop and mint one from the named record digests, or say you cannot because nobody named any. A record-ID list, an issue comment, a Slack "looks right", and a report are each how an approval set is minted — never a substitute for one. |
apply-plan or validate-approval returns stale (exit 3) |
Stop. Report the verdict naming every stale reason code. The recovery is the engine's: re-run the audit, mint afresh against the new report. Nothing was written; do not write anything. |
| The verdict is invalid (exit 1) | Stop and report every problem. An invalid artifact is a forgery or a bug, not a state to work around. |
The report is clean, or a record you were given is not in it |
Stop. The inputs disagree; never guess which record was meant. |
| A record you were not given is obviously right | Surface it. Unapproved is unapproved, and an unminted record cannot reach a plan at all. |
A record's code authorizes no operations — POLICY, ANCHOR-MISSING, ANCHOR-MALFORMED, ANCHOR-UNVERIFIABLE, ANCHOR-FUTURE-DATED, ANCHOR-UNRESOLVABLE-REFERENCE, or anything a newer audit policy introduces |
Stop and surface it. RECORD_REMEDIES is closed and fail-shut: a code nobody listed authorizes no operation, so there is no plan to write. POLICY is a legacy bulk verdict the bloat engine retired in favor of enumerable RETIRE-DOC scopes. Five of the six ANCHOR-* codes need a > As of line a human authors, not a span edit anyone approved — minting refuses to select one of them at all, so you will not carry one this far into the flow. ANCHOR-STALE is not one of these five: its remedy rewrites the anchor line like any other span edit, so do not lump it in with its siblings just because the family name matches — check a record's own code against this table, never the ANCHOR- prefix alone, before deciding it is a dead end. |
| Audit artifacts (report, verdicts, plan) sitting in the work tree | Move them to ${TMPDIR:-/tmp}/ (or another git-ignored path), do not commit them. Committing moves base_commit and stales the approval you just minted — the recovery is re-running the audit, which re-dirties the tree the same way. Relocate, never commit. |
Never edit the approval set, the report, or the plan's declared digests to make a
refusal go away. Repairing a stale base_commit, recomputing a digest over altered
records, or hand-widening scope.paths is forging authority — the exact attack the
contract exists to refuse. The same goes for hand-applying a fix "since the approval
was fine ten minutes ago": a stale approval set authorizes nothing at all.
And never move the repository to match the approval. Resetting, reverting, or
checking out an older commit so approval-base-commit-changed stops firing is the same
forgery from the other side, and it is the one the "clean working tree" requirement in
step 3 most invites — that requirement means commit or discard your own edits, never
rewind history until the refusal goes away. The approval set names the world it was
minted against; when the world moved, the artifact is what gets remade.
Minting is somebody's act, not a field you fill in. --minter names who performed
the semantic approval, so you may not run mint-approval on an absent person's behalf,
however confident you are of what they would say — a reviewer who approved this morning's
report has not approved this afternoon's. Re-running the audit is yours; minting against
the new report is theirs. (--minter-kind policy is refused here outright — a policy brand
says a standing declaration selected these records, and policy-mint is the only door that
produces one — so it is not your workaround either.)
Use the approval set you minted, or the one you were handed. Another approval-set file on disk that happens to validate is not a substitute for the one covering the records you were asked to land; check what it selects and what report it binds to, and say so, rather than shopping for whichever artifact clears the gate.
Distillation — the distiller returns operations, it does not write
An approved DISTILL record dispatches doc-lifecycle:doc-distiller with that one
record, its artifact path, its evidence, and the report path (it deduplicates its
landings against sibling records, which it can only do if it can see them). The
distiller owns the method — the landing re-verify, the per-section insight walk,
code-verified claims, one decision-log entry.
What comes back is edit-plan operations, not a changed working tree: the residue as
one create-document at the record's destination (a durable document that does not
exist yet — the audit refuses a destination that does — carrying its > As of first
line), and the planning artifact as retire-document. You fold those operations into the
plan the applier executes. The distiller writes no files, git rms nothing, stages
nothing.
One record authorizes exactly two paths — its own document and its destination — so
residue belonging in a third document (a decision-log entry when the destination is not
the log, an inbound reference that now points at a retired artifact) comes back
reported, not emitted. Raise those for their own approval and their own plan; an
operation reaching a path the record never named is
plan-target-not-record-target, and hand-editing it instead is the thing this whole
flow refuses.
A record carrying no destination authorizes one path, the artifact — so the whole
residue comes back reported and only retire-document is plannable. Do not land that
plan on its own without saying so: it deletes a planning artifact and strands
everything the distillation extracted. Present the drafted residue with it and let the
person decide whether to withhold the retirement until its residue has a home.
Land what verified. A claim the distiller could not verify is simply not in the operations it returned — surface the failure; never redraft the claim yourself to force it through, and never re-edit a landed result it flagged as a collision.
A DISTILL record whose status is pending-implementation is never actionable: there
is no landed code to verify claims against, so skip it with a note even when it was
approved. The planning document's own > Status: marker is the authority for that
status, not the record and not your reading of the plan — the engine refuses a verdict
whose status disagrees with the file, and an absent or malformed marker reads as
pending-implementation. So "the plan is really done, the marker is just out of date" is
something to surface, never something you decide.
Red flags — STOP
- Reaching for
Edit,Write, orsedagainst a document under audit → the applier is the only writer, always. - "There's no approval set, but the report says STALE and the fix is one number" → the report is proof of examination, deliberately not authority. Mint first.
- "The lead approved it in the issue / in Slack / in review" → ask which record digests. An approver who named no digest approved no record, and minting in their name notarizes authority they never exercised. Go back with the report; the selection is the approval.
- Exit 3 and you are about to apply the edit anyway → stale authorizes nothing. Re-run the audit, mint afresh.
- About to open
approval.jsonin an editor → forging authority. Never. - About to
git reset,git revert, or check out an older commit so the approval stops reading stale → same forgery, other side. The artifact gets remade, not the repository. - About to mint with an absent reviewer's name because "they already approved this morning" → minting is their act. Re-run the audit and hand it back.
- Attaching a
retire-document(or any operation the record's code does not authorize) to a record's plan because it is what the fix "really needs" → the remedy is the record's; a plan that picks the operation puts the choice back with the model. - An operation reaching a passage outside the approved record's units → out of scope, even one paragraph away, even when the report drafted that neighbour's text for you.
- Committing, pushing, or opening a PR at the end of the run → change approval is the person's, not yours.
- The distiller writing files,
git rm-ing the artifact, or staging a commit → it returns operations; the applier writes. - Reaching for
fixing-doc-drift,fixing-doc-bloat, orreferences/apply-discipline.md→ all three are retired. This skill and the applier contract replaced them.
Rationalization table
| Excuse | Reality |
|---|---|
| "The report already lists the record, so it's approved" | A report is proof of what was examined, not authority. Only an approval set authorizes, and only a person or a configured auto-apply policy mints one. |
| "The ID list I was handed is the approval" | It is how an approval set is minted, never a substitute for one. Mint it and let the engine validate it. |
| "The lane is blocked / it ships today — minting costs minutes I don't have" | Minting is one command over digests the report already carries; the flow is a couple of minutes, and it is the same couple of minutes whether or not anyone is waiting. Deadline pressure is when an unauthorized diff is least likely to be caught, which is exactly why it is not when the rule bends. |
"I'll place my own wording in the approved span — it reads better than the report's fix" |
The approved units bound where you write, not what. Text you authored inside an approved span is a diff the applier certified and nobody approved. |
| "It's one line — the applier is overkill for this" | The applier is what makes it one reviewable line: preimage checked, scope confined, provenance recorded. A hand edit is an unauthorized diff of exactly the same size. |
| "The approval went stale on an unrelated commit — the doc didn't change" | Stale authorizes nothing, and you do not get to decide which staleness was harmless. Re-run the audit and mint afresh; it is cheap. |
| "I'll just fix the base_commit field so it validates" | That is forging authority. The digest exists so every tamper is "delete one field". |
| "I'll roll the repo back to the commit the approval names, apply, then roll forward" | Moving the world to match the artifact is the same forgery as moving the artifact to match the world. The remedy is a fresh mint, in both directions. |
| "The reviewer approved this morning, so I'll re-mint in their name" | A reviewer who approved this morning's report has not approved this afternoon's. Re-run the audit; hand the mint back to them. |
| "There's another approval set on disk and it validates clean" | Check what it selects and which report it binds to. An artifact that clears the gate is not an artifact covering your records. |
| "The working tree has an unrelated edit, I'll apply on top" | apply-working-tree-not-clean. The applier certifies the whole diff, so it applies onto the committed baseline only. Commit or discard first. |
| "This is a bloat record, so I need the bloat fix skill" | There is one door. The finding code routes the remedy inside it. |
| "Distilling inline is faster than dispatching" | The distiller owns the method — re-verify, insight walk, dedup, decision log. Inlining drops all four, and writing files drops the applier. |
| "The distiller staged a commit, so I'll just commit it" | It returns operations. A staged commit means it broke its contract; report that rather than laundering it. |
| "The unapproved record next to my edit is obviously right too" | Its digest was never minted, so no plan can carry it. Surface it for the next approval. |