Rewrite — Truth-Preserving Document Re-voicing
Make a set of documents true against a trusted corpus, then regenerate them in a chosen voice — with an outline in between so the voice pass cannot drag the facts off-true.
You ARE the orchestrator. Follow these phases in order. The pipeline is two Workflow runs with a human checkpoint between them, because a Workflow cannot pause mid-run for input. Launch the first, do the interactive work while it runs, resolve disputes when it returns, then launch the second.
Setup
Resolve paths once before starting:
PRESETS=~/.claude/skills/rewrite/scripts/presets.sh
RUNDIR="$(mktemp -d)/rewrite" # frozen corpus spec, resolutions, and per-run artifacts
Create RUNDIR with mkdir -p. Everything you freeze (corpus spec, dispute resolutions) goes there so the run is auditable.
Argument Parsing
Arguments can appear in any order:
| Argument | Form | Default |
|---|---|---|
| targets | Bare path/glob (first non-flag argument) | (required) |
--sources |
Path/glob or a natural-language description | (required) |
--voice |
preserve | exemplar:<file> | preset:<name> | design |
preserve |
--save-preset |
Name to save the resolved style spec under | none |
--verifiers |
Integer, refute-verifiers per unique claim | 3 |
--dry-run |
Flag (no value) | false |
--max-agents |
Integer, concurrency cap | 12 |
--output-suffix |
String inserted before the extension | .rewritten |
If targets or --sources is missing, stop and say which — the corpus is the oracle; without it there is nothing to check claims against.
Phase 0: Frame and resolve the corpus spec
Step 1: Resolve the corpus spec
The corpus spec is one artifact — {files: [...], authority: [...], scope: [...]} — however --sources was given.
- Path/glob: expand it to a file list with Glob. A bare directory means every text file under it, recursively; skip binaries and anything matched by
.gitignore.authorityandscopeare empty. - Description: dispatch one
Agent(subagent_type: Explore) to resolve the sentence against the real filesystem into the same shape. The description may carry authority ordering ("trust the CHANGELOG over the prose on versions") and per-source scope ("metrics.csv for numbers, the RFC for behavior") — capture these asauthorityandscoperules. Reject any clause that resolves to no file (e.g. "what you know about HTTP") and report it; it would readmit model priors, which the corpus oracle exists to exclude.
Write the resolved spec to $RUNDIR/corpus-spec.json and show the user the file list plus any authority/scope rules. Wait for confirmation before verifying. This freeze is what keeps verification deterministic and auditable even when the selection came from a sentence.
Step 2: Inventory and safety checks
- Expand
targetsto a file list. Count and total the size of targets and corpus files. - Detect target/corpus overlap. If any file is in both sets, warn and list them — a document cannot be its own oracle — and proceed only on confirmation.
- If the projected dispute volume looks high (a thin corpus against many target claims), say so now rather than at the checkpoint.
Step 3: Confirm the Workflow opt-in
Estimate scale: roughly unique_claims × --verifiers + targets × 3 agents. Tell the user the rough agent count and that this runs as an opt-in Workflow. If the user declines, stop and do nothing.
Tools: Bash, Glob, Read, Agent (description resolution), Write (freeze the corpus spec).
Phase 1: Launch fact-check (background) + acquire style spec (foreground)
Start both tracks together. Track A runs unattended; Track B is where you spend the wait.
Track A: Workflow 1 — fact-check
Call Workflow with this inline script, passing args: { targets: [...paths], corpus: {files, authority, scope}, verifiers: N }. It extracts claims, deduplicates them semantically, and verifies each against the corpus.
export const meta = {
name: 'rewrite-factcheck',
description: 'Extract, semantically dedup, and adversarially verify claims against a trusted corpus',
phases: [{ title: 'Extract' }, { title: 'Dedup' }, { title: 'Verify' }],
}
const CLAIMS_SCHEMA = {
type: 'object',
properties: {
claims: {
type: 'array',
items: {
type: 'object',
properties: {
text: { type: 'string' }, // one atomic factual proposition
location: { type: 'string' }, // section/line hint within the doc
},
required: ['text'],
},
},
},
required: ['claims'],
}
const DEDUP_SCHEMA = {
type: 'object',
properties: {
claims: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
text: { type: 'string' }, // canonical phrasing
docs: { type: 'array', items: { type: 'string' } },
variants_conflict: { type: 'boolean' }, // true if merged variants assert incompatible things
},
required: ['id', 'text', 'docs'],
},
},
},
required: ['claims'],
}
const REFUTE_SCHEMA = {
type: 'object',
properties: {
supported: { type: 'boolean' }, // corpus affirmatively supports the claim
refuted: { type: 'boolean' }, // corpus contradicts the claim
correction: { type: 'string' }, // if refuted: what the corpus says instead
evidence: { type: 'string' }, // corpus file:excerpt cited
},
required: ['supported', 'refuted'],
}
const corpusList = args.corpus.files.join('\n')
const rules = [
args.corpus.authority?.length ? `Authority (higher wins a conflict): ${args.corpus.authority.join('; ')}` : '',
args.corpus.scope?.length ? `Per-source scope: ${args.corpus.scope.join('; ')}` : '',
].filter(Boolean).join('\n')
// 1. Extract — fan out over target documents.
phase('Extract')
const perDoc = await parallel(args.targets.map(path => () =>
agent(
`Read ${path}. Extract every atomic factual proposition it asserts — one checkable fact per claim, no opinions or instructions. Give a short location hint for each.`,
{ label: `extract:${path}`, phase: 'Extract', schema: CLAIMS_SCHEMA },
).then(r => r && r.claims.map(c => ({ ...c, doc: path })))
))
const rawClaims = perDoc.filter(Boolean).flat()
// 2. Dedup — barrier: one merge over the whole set (semantic, not string).
phase('Dedup')
const deduped = await agent(
`Here are ${rawClaims.length} raw claims, each with its source doc:\n${JSON.stringify(rawClaims)}\n\n` +
`Merge claims that assert the SAME fact even when worded differently ("founded in 1998" == "established 23 years ago"). ` +
`Give each merged claim a stable id, a canonical phrasing, and the list of docs that asserted it. ` +
`If merged variants actually assert incompatible things, keep them as one claim but set variants_conflict=true. ` +
`When unsure whether two claims are the same, keep them separate — false merges lose facts.`,
{ label: 'dedup', phase: 'Dedup', schema: DEDUP_SCHEMA },
)
// 3. Verify — fan out over unique claims; N refuters each.
phase('Verify')
function classify(claim, votes) {
if (votes.length < Math.ceil(args.verifiers / 2)) {
return { ...claim, verdict: 'disputed', reason: 'too few verifier votes to decide' }
}
const refuted = votes.filter(v => v.refuted)
const supported = votes.filter(v => v.supported && !v.refuted)
if (claim.variants_conflict) {
return { ...claim, verdict: 'disputed', reason: 'target documents assert conflicting versions' }
}
if (refuted.length > votes.length / 2) {
return { ...claim, verdict: 'refuted', correction: refuted.find(v => v.correction)?.correction || '', evidence: refuted[0].evidence }
}
if (supported.length > votes.length / 2 && refuted.length === 0) {
return { ...claim, verdict: 'confirmed', evidence: supported[0].evidence }
}
if (supported.length === 0 && refuted.length === 0) {
return { ...claim, verdict: 'disputed', reason: 'corpus is silent on this claim' }
}
return { ...claim, verdict: 'disputed', reason: 'verifiers split' }
}
const verdicts = await parallel(deduped.claims.map(claim => () =>
parallel(Array.from({ length: args.verifiers }, (_, i) => () =>
agent(
`Corpus files (the ONLY evidence you may cite):\n${corpusList}\n${rules}\n\n` +
`Claim: "${claim.text}"\n\n` +
`Try to REFUTE this claim using the corpus. Read the relevant corpus files. ` +
`Set refuted=true only if the corpus contradicts it (give the correction). ` +
`Set supported=true only if the corpus affirmatively supports it. ` +
`If the corpus says nothing either way, set both false. Cite the file and excerpt you relied on.`,
{ label: `verify:${claim.id}#${i}`, phase: 'Verify', schema: REFUTE_SCHEMA },
)
)).then(votes => classify(claim, votes.filter(Boolean)))
))
return { verdicts: verdicts.filter(Boolean) }
Tell the user briefly: "Fact-checking N documents against the corpus in the background. Meanwhile, let's settle the voice." The call runs in the background — you will get a task notification when it returns.
Track B: acquire the style spec
Resolve --voice now, in the main conversation, while Workflow 1 runs:
| Mode | What you do |
|---|---|
preserve (default) |
No interaction. The style spec is each target's own voice — regeneration keeps it. Record { mode: "preserve" }. |
exemplar:<file> |
Read the file. Derive a style spec from it (voice, format, lexicon, constraints). |
preset:<name> |
bash "$PRESETS" show <name> and load the returned JSON. |
design |
Co-author the spec with the user: propose a draft, generate a short sample paragraph from it, take reactions, refine. This is the interactive track that overlaps Workflow 1. |
A style spec is JSON: { name, voice{tone,person,register}, format{structure,headings,length_target}, lexicon{preferred,avoided}, constraints{must,must_not}, exemplar_excerpt? }.
If --save-preset <name> was set, after the spec is settled write it to a temp file and run bash "$PRESETS" add <name> --tier user --from-json <file> — after the user confirms.
Tools: Workflow (Track A); Read, Write, Bash, Agent (Track B).
Phase 2: Dispute resolution (checkpoint)
When Workflow 1 returns, partition verdicts into confirmed, refuted (each with its correction), and disputed.
If disputed is empty, skip this phase silently.
Otherwise present the disputed claims as one batch — never one document at a time. For each: the claim, the reason (corpus silent, verifiers split, target documents conflict, too few votes), the documents that assert it, and any corpus excerpts the verifiers cited. Ask the user to resolve each as:
- true — keep as asserted;
- false — drop it, or correct it to a stated value;
- rephrase — restate it as the user dictates.
Write the resolutions to $RUNDIR/resolutions.json keyed by claim id. Because claims are deduped corpus-wide, each resolution propagates to every document in that claim's docs list.
Tools: Write, wait for user input.
Phase 3: Regenerate (Workflow 2, background)
Assemble the final corrected claim set per document: confirmed + refuted corrections + resolved disputes, dropping anything the user marked false. Build args.docs = [{ path, corrections: [...] }, ...] where corrections are the claims (with final text) that apply to that document.
If --dry-run was set, stop here: report the correction and dispute counts and the outlines that would be produced, and write nothing.
Otherwise call Workflow with args: { docs, styleSpec, suffix, retries: 2 }:
export const meta = {
name: 'rewrite-regenerate',
description: 'Per document: apply corrections, outline, regenerate under a style spec, gate for fidelity',
phases: [{ title: 'Apply' }, { title: 'Outline' }, { title: 'Regenerate' }],
}
const OUTLINE_SCHEMA = {
type: 'object',
properties: { outline: { type: 'array', items: { type: 'string' } } }, // ordered points, content only
required: ['outline'],
}
const REGEN_SCHEMA = {
type: 'object',
properties: { text: { type: 'string' } },
required: ['text'],
}
const GATE_SCHEMA = {
type: 'object',
properties: {
unsourced: { type: 'array', items: { type: 'string' } }, // factual assertions not traceable to the outline
},
required: ['unsourced'],
}
const styleDesc = args.styleSpec.mode === 'preserve'
? 'Preserve THIS document's own voice, register, and format.'
: `Voice/format spec:\n${JSON.stringify(args.styleSpec)}`
// Pipeline over documents — no barrier; each doc flows through all stages independently.
const results = await pipeline(args.docs,
// 1. Apply corrections to the source doc.
(doc) => agent(
`Read ${doc.path}. Apply these corrections, changing only what they require and leaving everything else intact:\n${JSON.stringify(doc.corrections)}\n\nReturn the full corrected document text.`,
{ label: `apply:${doc.path}`, phase: 'Apply', schema: REGEN_SCHEMA },
),
// 2. Summarize the corrected doc to a content-only outline.
(applied, doc) => agent(
`Reduce this corrected document to an ordered outline of its factual points — content and logical order only, no voice or formatting:\n\n${applied.text}`,
{ label: `outline:${doc.path}`, phase: 'Outline', schema: OUTLINE_SCHEMA },
),
// 3. Regenerate from the outline under the style spec, then gate for fidelity (bounded retries).
async (outlineResult, doc) => {
const outline = outlineResult.outline
let attempt = 0, out = null
while (attempt <= args.retries) {
const tighten = attempt === 0 ? '' : ' Assert NOTHING that is not in the outline — a previous attempt added unsupported detail.'
out = await agent(
`Regenerate a complete document from this outline. ${styleDesc}\n` +
`Every factual assertion must come from the outline; add no facts of your own.${tighten}\n\nOutline:\n${JSON.stringify(outline)}`,
{ label: `regen:${doc.path}#${attempt}`, phase: 'Regenerate', schema: REGEN_SCHEMA },
)
const gate = await agent(
`Outline:\n${JSON.stringify(outline)}\n\nDocument:\n${out.text}\n\nList every FACTUAL assertion in the document that does not trace to a point in the outline. Ignore pure connective/stylistic phrasing.`,
{ label: `gate:${doc.path}#${attempt}`, phase: 'Regenerate', schema: GATE_SCHEMA },
)
if (!gate.unsourced.length) return { path: doc.path, text: out.text, flags: [] }
attempt++
if (attempt > args.retries) return { path: doc.path, text: out.text, flags: gate.unsourced }
}
},
)
return { results: results.filter(Boolean) }
When Workflow 2 returns, write each result to <path><suffix>.<ext> with the Write tool (e.g. report.md → report.rewritten.md). Originals are never touched. If a result has flags, note them in the report and mark them inline in the written file so the user can review the assertions the gate could not source.
Tools: Workflow, Write.
Phase 4: Report
Summarize plainly:
- documents rewritten and their output paths;
- unique claims checked, how many corrected, how many disputes and how each was resolved;
- any fidelity-gate flags that survived retries, per document.
Optionally route a completion notice through the notify skill.
Tools: Read, Write (optional notify).
Error Recovery
| Situation | Response |
|---|---|
targets or --sources missing |
Stop; state which is missing and that the corpus is required as the oracle. |
A --sources description clause resolves to no file |
Reject that clause in Phase 0 and report it. If nothing resolves, treat as missing sources. |
| Targets and corpus overlap | Warn, list the files, proceed only on confirmation. |
| User declines the Workflow opt-in | Stop after Phase 0; do nothing. |
| Corpus internally contradictory on a claim | If an authority rule covers it, resolve by the rule. Otherwise mark disputed with the conflict as its reason. |
| A verifier agent dies | Its vote drops; classify sends claims with too few votes to disputed, never to confirmed. Never re-launch. |
| Dedup uncertain whether two claims match | Keep them separate (the prompt says so) — false merges lose facts. |
disputed empty |
Skip Phase 2. |
No corrections and --voice preserve |
Nothing would change; report that and skip Phase 3. |
| Fidelity gate cannot converge after retries | Write the document, flag the unsourced assertions inline and in the report. Do not silently drop or keep them. |
preset:<name> not found |
bash "$PRESETS" list and ask for a valid name or another voice mode. |
| User interrupts while a Workflow is running | Workflow calls run to completion in the background; continue from whatever the run returns once it reports. |