Detecting Doc Bloat
Drift asks whether a doc is accurate; bloat asks whether it still earns
its tokens. This skill is a thin router: the engine owns scope,
segmentation, membership, and validation; per-need references carry the
verdict rules; you supply judgment one bounded chunk at a time. Three
non-negotiables:
- A verdict requires evidence. Every verdict names the quote, code line,
or grep that proves the finding — never "feels redundant".
- The result is structured, not prose. Verdicts per
output-contract.md,
shape-checked at every seam and audited by the engine. Approval of a
record's digest is the only bridge from a finding to a file change.
- Read-only — this skill never edits. A human approves digests;
fixing-docs applies the approved subset.
Every engine command below is python3 -m doclifecycle … with the plugin's
engine/ directory on PYTHONPATH (plugins/doc-lifecycle/engine/README.md
covers both spellings).
Doc kinds (the planner hints these; override only with stated evidence)
- living — claim-style docs tracking the repo (README, CLAUDE.md,
runbooks, reference). Rules:
references/verdict-lenses.md.
- narrative — opens with growing-docs'
> As of anchor (the file's first
line, or the first line under the title), wherever the file sits. Own bar;
never a planning artifact. Rules: verdict-lenses.md.
- planning — designs/specs/plans describing an intended change. Rules:
references/planning-artifacts.md.
A directory of ephemeral artifacts is not a fourth kind: it is one
RETIRE-DOC verdict over an enumerable scope, whose members the engine
expands from the index (output-contract.md). That replaced the legacy
POLICY record and its policy_scope config — a config still declaring the
key plans normally, and the planner says so.
The audit (run these steps, in order)
Every artifact this audit writes — the plan, the verdicts, the report — goes to
${TMPDIR:-/tmp}/, never the work tree: fixing-docs' applier confines a run to its
approval set's paths, so an audit artifact sitting in the tree reads as an unaccounted
change and the apply refuses (apply-working-tree-not-confined). The steps and the
invocation templates below reuse that same destination; only the filename changes per
artifact.
- Plan the chunks. Requires
.doc-lifecycle/registry.json — this whole
audit is a walk of the registered corpus, and an unregistered repo has
none. No registry yet: bootstrapping-docs writes one for a repo with no
doc set; for a repo whose docs exist but nothing classifies them yet, run
its migration-draft --registry-only step (scheduling-doc-sync's
"Migration to the registry contract" is the one owner of that sequence).
python3 -m doclifecycle bloat-plan --repo . > "${TMPDIR:-/tmp}/bloat-plan.json" partitions every indexed document into bounded chunks
(--max-documents, --max-units), content-addressed so an unchanged chunk
keeps its id, and refuses outright (registry-missing) when there is none.
For a dispatched sweep, scripts/plan-chunks.py adds the dispatch
ergonomics the engine has no opinion about: per-chunk turn budgets,
--emit-prompt slices, and --results-dir resume. On a registered
repository it asks the public bloat-plan command for the authentic plan,
dispatches those exact chunk ids and members, and preserves that full plan
in engine_plan; assembly refuses a missing, edited, or differently
partitioned plan before any envelope can reach bloat-audit. The script's
legacy .md/audit-scope.json planner remains only for unregistered
inventory and prompt-planning uses; such a manifest cannot be assembled
into completion evidence.
Either way bloat-audit re-derives every fact from the registry, so a
chunk plan is a work order, never an authority. plan-chunks.py --emit-prompt/
--emit-turns read a bloat-plan manifest too (its chunks carry
documents, bare paths, rather than plan-chunks.py's own per-doc
docs: [{"path","lines","hint"}]) — the dispatch prompt renders whichever
fields a chunk's dialect supplies; the turn budget falls back to the floor
only for plan-chunks.py's own dialect; a bloat-plan chunk has no
turns to fall back from and the script says so rather than guessing.
- Judge each chunk. Small scope (≲2 chunks): sweep inline with the
reference rules. Large scope: never sweep inline — the manifest is your
work order as orchestrator (do not enumerate or read the corpus yourself),
and you dispatch one subagent per pending chunk, in concurrent waves of
several, never serially (chunks are independent; a serial walk of a
bootstrap-scale manifest is hours of avoidable wall-clock). Render each
dispatch with
--emit-prompt (which requires --results-dir, the same
out-of-work-tree directory plan-chunks.py was planned with — the
rendered prompt names an absolute path under it, never a bare relative
one) and point the subagent at (i) output-contract.md and (ii) only the
reference file(s) its chunk's kinds need. Each subagent writes
{"chunk": "<id>", "verdicts": [...]} to the out-of-tree path the prompt
names, e.g. <dir>/chunks/<id>.json — never a path inside the repository.
- Name the content. A verdict about one document names the unit
digests it covers, from
python3 -m doclifecycle segment --repo . --path <path> — copied verbatim, never invented or abbreviated. A bulk
RETIRE-DOC names a scope instead, and nothing else.
- Shape-check every seam: each chunk result as it lands, then the
assembled envelope. A failing chunk is re-dispatched fresh once, then
you stop and name it.
- Run the audit.
python3 -m doclifecycle bloat-audit --repo . --verdicts "${TMPDIR:-/tmp}/bloat-verdicts.json" > "${TMPDIR:-/tmp}/bloat-report.json" checks every verdict against the
whole-repository context index — membership, destinations, units,
file-bound DISTILL status — expands each scope into one finding per
member, and writes the validated report. It fails closed: any problem
records nothing and names everything, so one re-prompt fixes all of it.
Exit 0 is a report (clean or with findings), 1 refused, 4 partial. Every
valid chunk contributes positive examined entries bound to its chunk and
plan digest. A missing or invalid chunk contributes one incomplete entry
per affected document, naming the chunk, and forces partial. Stale,
duplicated, mismatched, or edited completion evidence is invalid rather
than partial: re-plan and run the pending chunks again.
Headless (chunk executor): your chunk slice arrived verbatim in the
dispatch prompt — the doc list and the output path. That slice is your entire
scope: judge exactly those docs with the reference rules, write the chunk
result, stop. You never open the manifest — it is the orchestrator's state,
and it may not even be on disk; budgets, retries, and assembly are likewise
the workflow's, not yours.
Script invocation templates
# plan (inventory -> chunk manifest; size + projected invocations on stderr).
# To narrow scope, pass --config with exclude/include globs (include re-adds what
# it matches); the chunking keys are documented in the script docstring.
# Chunk ids are content-addressed, so --results-dir resume skips only chunks
# whose docs are unchanged; each chunk carries its model-invocation turn budget.
# <dir> is outside the work tree (e.g. "${TMPDIR:-/tmp}/bloat") — same confinement
# reason as the plan/verdicts/report artifacts above.
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/plan-chunks.py \
--out <dir>/manifest.json --results-dir <dir>/chunks
# render one chunk's dispatch prompt / turn budget (slice verbatim — the
# executor never opens the manifest). --results-dir is required for
# --emit-prompt: the rendered prompt names an absolute path under it as the
# write destination, not the bare "chunks/<id>.json" a work-tree-rooted
# executor would resolve straight into the repository.
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/plan-chunks.py \
--emit-prompt <id> --manifest <dir>/manifest.json --results-dir <dir>/chunks
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/plan-chunks.py \
--emit-turns <id> --manifest <dir>/manifest.json
# shape-check one chunk result
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/validate-bloat-output.py \
--chunk <dir>/chunks/<id>.json --manifest <dir>/manifest.json
# assemble every chunk result into the verdicts + completion envelope (refuses
# partial assembly; --allow-partial records missing/invalid chunks as gaps)
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/validate-bloat-output.py \
--assemble <dir>/chunks --manifest <dir>/manifest.json --out <dir>/bloat-verdicts.json
# shape-check the envelope, then audit it
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/validate-bloat-output.py \
<dir>/bloat-verdicts.json
python3 -m doclifecycle bloat-audit --repo . --verdicts <dir>/bloat-verdicts.json \
> <dir>/bloat-report.json
The contract
Verdicts carry only id, verdict, path, units, evidence, destination, proposal, status, scope, sample; the verdicts are CUT / CONDENSE / EXTRACT-AND-MOVE / MERGE-DOC / RETIRE-DOC / DISTILL; the artifact is the
envelope {"schema_version": 1, "verdicts": [...], "completion": {...}}.
The assembler authors completion; chunk executors author only verdicts.
files, members,
occurrences, and contention are refused outright — a bulk finding's
members are enumerated from the index, never asserted by the model.
DISTILL verdicts carry classification + landed-code evidence, plus — where
you can say where the residue belongs — an optional destination, a path
nobody has written yet. Never the residue itself: the
claims/insights/decision-entry authoring is the doc-distiller agent's
post-approval job, dispatched by fixing-docs. A DISTILL naming no
destination is a retire-only distillation, legal and lossy exactly when the
residue lands under no record — so omit it because there is nothing to place,
not by default. Field rules, the worked
example, and the chunk-result seam shape: output-contract.md. The
shape checker sees shape; bloat-audit is the authority on everything else.
Never hand off anything either one rejects.
REQUIRED SUB-SKILL: use writing-docs for every replacement or
extraction text you propose (CONDENSE and EXTRACT-AND-MOVE proposals) —
dense, anchored, no narrative.
Presenting to a human
When a human triages in-session, summarize the report — never paste raw JSON
as the summary. Group by path, one line per record: its code, its
evidence, and for a DISTILL its status. Then ask which records to apply.
Nothing you present is authorization on its own; what fixing-docs receives
is the human's selection, minted from each record's digest (mint-approval --record <digest>) — the id is a label the report may renumber.
Red flags — STOP
- A prose report with no structured verdicts, or an invented verdict → the
six enum values, the contract shape, nothing else.
- Evidence asserted, not shown ("the sections are identical", no quote) → go
get the line or the quoted overlap.
- Naming a unit you did not read out of
segment — a paraphrase, a line
number, a truncated digest → the digest verbatim, or no verdict.
- Listing the files a bulk retirement covers (a
files key, or a scope
narrowed to the paths you happened to read) → declare the inclusion rule and
let the engine enumerate; a sample records what you read and authorizes
nothing.
- Skipping the shape check at a seam — chunk results as they land, the
assembled envelope → run it; never hand off a result it rejects. (As a
headless executor, seam validation is the workflow's own step — never a
license to open the manifest.)
- Authoring DISTILL claims/insights/decision entries at detect time — anywhere,
including inside
evidence → post-approval distiller work; emit the
classification and proof only.
- Opening the manifest, or enumerating the corpus, as a chunk executor →
your slice arrived in the dispatch prompt; audit exactly it and stop.
- Sweeping inline when the planner projects >2 chunks → dispatch per chunk;
the manifest is the orchestrator's work order, each executor's is its
dispatched slice.
- Editing, deleting, or "just fixing the small one" → read-only; surface it as
a verdict and stop.
1---2name: detecting-doc-bloat3description: Use when auditing documentation for low-value content — redundant, verbose, duplicated, or past its useful form — proposing pruning/condensing/distillation, and whenever bloat analysis runs programmatically (nightly sweep, PR gate, or a chunk-executor invocation handed its chunk slice) and must emit a structured, parseable proposal. Read-only — it proposes, a human approves, fixing-docs applies.4---56# Detecting Doc Bloat78**Drift asks whether a doc is *accurate*; bloat asks whether it still *earns9its tokens*.** This skill is a thin router: the engine owns scope,10segmentation, membership, and validation; per-need references carry the11verdict rules; you supply judgment one bounded chunk at a time. Three12non-negotiables:13141. **A verdict requires evidence.** Every verdict names the quote, code line,15 or grep that proves the finding — never "feels redundant".162. **The result is structured, not prose.** Verdicts per `output-contract.md`,17 shape-checked at every seam and audited by the engine. Approval of a18 record's digest is the only bridge from a finding to a file change.193. **Read-only — this skill never edits.** A human approves digests;20 **`fixing-docs`** applies the approved subset.2122Every engine command below is `python3 -m doclifecycle …` with the plugin's23`engine/` directory on `PYTHONPATH` (`plugins/doc-lifecycle/engine/README.md`24covers both spellings).2526## Doc kinds (the planner hints these; override only with stated evidence)2728- **living** — claim-style docs tracking the repo (README, CLAUDE.md,29 runbooks, reference). Rules: `references/verdict-lenses.md`.30- **narrative** — opens with growing-docs' `> As of` anchor (the file's first31 line, or the first line under the title), wherever the file sits. Own bar;32 never a planning artifact. Rules: `verdict-lenses.md`.33- **planning** — designs/specs/plans describing an intended change. Rules:34 `references/planning-artifacts.md`.3536A directory of ephemeral artifacts is **not** a fourth kind: it is one37`RETIRE-DOC` verdict over an enumerable `scope`, whose members the engine38expands from the index (`output-contract.md`). That replaced the legacy39`POLICY` record and its `policy_scope` config — a config still declaring the40key plans normally, and the planner says so.4142## The audit (run these steps, in order)4344Every artifact this audit writes — the plan, the verdicts, the report — goes to45`${TMPDIR:-/tmp}/`, never the work tree: `fixing-docs`' applier confines a run to its46approval set's paths, so an audit artifact sitting in the tree reads as an unaccounted47change and the apply refuses (`apply-working-tree-not-confined`). The steps and the48invocation templates below reuse that same destination; only the filename changes per49artifact.50511. **Plan the chunks.** Requires `.doc-lifecycle/registry.json` — this whole52 audit is a walk of the *registered* corpus, and an unregistered repo has53 none. No registry yet: **bootstrapping-docs** writes one for a repo with no54 doc set; for a repo whose docs exist but nothing classifies them yet, run55 its `migration-draft --registry-only` step (**scheduling-doc-sync**'s56 "Migration to the registry contract" is the one owner of that sequence).57 `python3 -m doclifecycle bloat-plan --repo . >58 "${TMPDIR:-/tmp}/bloat-plan.json"` partitions every indexed document into bounded chunks59 (`--max-documents`, `--max-units`), content-addressed so an unchanged chunk60 keeps its id, and refuses outright (`registry-missing`) when there is none.61 For a dispatched sweep, `scripts/plan-chunks.py` adds the dispatch62 ergonomics the engine has no opinion about: per-chunk turn budgets,63 `--emit-prompt` slices, and `--results-dir` resume. On a registered64 repository it asks the public `bloat-plan` command for the authentic plan,65 dispatches those exact chunk ids and members, and preserves that full plan66 in `engine_plan`; assembly refuses a missing, edited, or differently67 partitioned plan before any envelope can reach `bloat-audit`. The script's68 legacy `.md`/`audit-scope.json` planner remains only for unregistered69 inventory and prompt-planning uses; such a manifest cannot be assembled70 into completion evidence.71 Either way `bloat-audit` re-derives every fact from the registry, so a72 chunk plan is a work order, never an authority. `plan-chunks.py --emit-prompt`/73 `--emit-turns` read a `bloat-plan` manifest too (its chunks carry74 `documents`, bare paths, rather than `plan-chunks.py`'s own per-doc75 `docs: [{"path","lines","hint"}]`) — the dispatch prompt renders whichever76 fields a chunk's dialect supplies; the turn budget falls back to the floor77 only for `plan-chunks.py`'s own dialect; a `bloat-plan` chunk has no78 `turns` to fall back from and the script says so rather than guessing.792. **Judge each chunk.** Small scope (≲2 chunks): sweep inline with the80 reference rules. Large scope: never sweep inline — the manifest is your81 work order as orchestrator (do not enumerate or read the corpus yourself),82 and you dispatch **one subagent per pending chunk, in concurrent waves of83 several, never serially** (chunks are independent; a serial walk of a84 bootstrap-scale manifest is hours of avoidable wall-clock). Render each85 dispatch with `--emit-prompt` (which requires `--results-dir`, the same86 out-of-work-tree directory `plan-chunks.py` was planned with — the87 rendered prompt names an absolute path under it, never a bare relative88 one) and point the subagent at (i) `output-contract.md` and (ii) only the89 reference file(s) its chunk's kinds need. Each subagent writes90 `{"chunk": "<id>", "verdicts": [...]}` to the out-of-tree path the prompt91 names, e.g. `<dir>/chunks/<id>.json` — never a path inside the repository.923. **Name the content.** A verdict about one document names the **unit93 digests** it covers, from `python3 -m doclifecycle segment --repo . --path94 <path>` — copied verbatim, never invented or abbreviated. A bulk95 `RETIRE-DOC` names a `scope` instead, and nothing else.964. **Shape-check every seam**: each chunk result as it lands, then the97 assembled envelope. A failing chunk is re-dispatched fresh **once**, then98 you stop and name it.995. **Run the audit.** `python3 -m doclifecycle bloat-audit --repo . --verdicts100 "${TMPDIR:-/tmp}/bloat-verdicts.json" > "${TMPDIR:-/tmp}/bloat-report.json"` checks every verdict against the101 whole-repository context index — membership, destinations, units,102 file-bound `DISTILL` status — expands each `scope` into one finding per103 member, and writes the validated report. It fails closed: any problem104 records nothing and names everything, so one re-prompt fixes all of it.105 Exit 0 is a report (clean or with findings), 1 refused, 4 partial. Every106 valid chunk contributes positive `examined` entries bound to its chunk and107 plan digest. A missing or invalid chunk contributes one `incomplete` entry108 per affected document, naming the chunk, and forces partial. Stale,109 duplicated, mismatched, or edited completion evidence is invalid rather110 than partial: re-plan and run the pending chunks again.111112**Headless (chunk executor):** your chunk slice arrived verbatim in the113dispatch prompt — the doc list and the output path. That slice is your entire114scope: judge exactly those docs with the reference rules, write the chunk115result, stop. You never open the manifest — it is the orchestrator's state,116and it may not even be on disk; budgets, retries, and assembly are likewise117the workflow's, not yours.118119## Script invocation templates120121```bash122# plan (inventory -> chunk manifest; size + projected invocations on stderr).123# To narrow scope, pass --config with exclude/include globs (include re-adds what124# it matches); the chunking keys are documented in the script docstring.125# Chunk ids are content-addressed, so --results-dir resume skips only chunks126# whose docs are unchanged; each chunk carries its model-invocation turn budget.127# <dir> is outside the work tree (e.g. "${TMPDIR:-/tmp}/bloat") — same confinement128# reason as the plan/verdicts/report artifacts above.129python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/plan-chunks.py \130 --out <dir>/manifest.json --results-dir <dir>/chunks131132# render one chunk's dispatch prompt / turn budget (slice verbatim — the133# executor never opens the manifest). --results-dir is required for134# --emit-prompt: the rendered prompt names an absolute path under it as the135# write destination, not the bare "chunks/<id>.json" a work-tree-rooted136# executor would resolve straight into the repository.137python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/plan-chunks.py \138 --emit-prompt <id> --manifest <dir>/manifest.json --results-dir <dir>/chunks139python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/plan-chunks.py \140 --emit-turns <id> --manifest <dir>/manifest.json141142# shape-check one chunk result143python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/validate-bloat-output.py \144 --chunk <dir>/chunks/<id>.json --manifest <dir>/manifest.json145146# assemble every chunk result into the verdicts + completion envelope (refuses147# partial assembly; --allow-partial records missing/invalid chunks as gaps)148python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/validate-bloat-output.py \149 --assemble <dir>/chunks --manifest <dir>/manifest.json --out <dir>/bloat-verdicts.json150151# shape-check the envelope, then audit it152python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-doc-bloat/scripts/validate-bloat-output.py \153 <dir>/bloat-verdicts.json154python3 -m doclifecycle bloat-audit --repo . --verdicts <dir>/bloat-verdicts.json \155 > <dir>/bloat-report.json156```157158## The contract159160Verdicts carry only `id, verdict, path, units, evidence, destination,161proposal, status, scope, sample`; the verdicts are `CUT / CONDENSE /162EXTRACT-AND-MOVE / MERGE-DOC / RETIRE-DOC / DISTILL`; the artifact is the163envelope `{"schema_version": 1, "verdicts": [...], "completion": {...}}`.164The assembler authors `completion`; chunk executors author only verdicts.165`files`, `members`,166`occurrences`, and `contention` are refused outright — a bulk finding's167members are enumerated from the index, never asserted by the model.168`DISTILL` verdicts carry classification + landed-code evidence, plus — where169you can say where the residue belongs — an optional `destination`, a path170nobody has written yet. Never the residue itself: the171claims/insights/decision-entry authoring is the `doc-distiller` agent's172post-approval job, dispatched by `fixing-docs`. A `DISTILL` naming no173destination is a *retire-only* distillation, legal and lossy exactly when the174residue lands under no record — so omit it because there is nothing to place,175not by default. Field rules, the worked176example, and the chunk-result seam shape: **`output-contract.md`**. The177shape checker sees shape; `bloat-audit` is the authority on everything else.178Never hand off anything either one rejects.179180**REQUIRED SUB-SKILL:** use **writing-docs** for every replacement or181extraction text you propose (`CONDENSE` and `EXTRACT-AND-MOVE` proposals) —182dense, anchored, no narrative.183184## Presenting to a human185186When a human triages in-session, summarize the report — never paste raw JSON187as the summary. Group by `path`, one line per record: its `code`, its188`evidence`, and for a `DISTILL` its `status`. Then ask which records to apply.189Nothing you present is authorization on its own; what `fixing-docs` receives190is the human's selection, minted from each record's **digest** (`mint-approval191--record <digest>`) — the id is a label the report may renumber.192193## Red flags — STOP194195- A prose report with no structured verdicts, or an invented verdict → the196 six enum values, the contract shape, nothing else.197- Evidence asserted, not shown ("the sections are identical", no quote) → go198 get the line or the quoted overlap.199- Naming a unit you did not read out of `segment` — a paraphrase, a line200 number, a truncated digest → the digest verbatim, or no verdict.201- Listing the files a bulk retirement covers (a `files` key, or a `scope`202 narrowed to the paths you happened to read) → declare the inclusion rule and203 let the engine enumerate; a `sample` records what you read and authorizes204 nothing.205- Skipping the shape check at a seam — chunk results as they land, the206 assembled envelope → run it; never hand off a result it rejects. (As a207 headless executor, seam validation is the workflow's own step — never a208 license to open the manifest.)209- Authoring DISTILL claims/insights/decision entries at detect time — anywhere,210 including inside `evidence` → post-approval distiller work; emit the211 classification and proof only.212- Opening the manifest, or enumerating the corpus, as a chunk executor →213 your slice arrived in the dispatch prompt; audit exactly it and stop.214- Sweeping inline when the planner projects >2 chunks → dispatch per chunk;215 the manifest is the orchestrator's work order, each executor's is its216 dispatched slice.217- Editing, deleting, or "just fixing the small one" → read-only; surface it as218 a verdict and stop.