# doc-hygiene Claude Code plugin that monitors and manages **stale** and **bloated** project documentation. Installed globally, operates per-project. Reminds (deterministic, zero-token) on `SessionStart`; checks and cleans on demand via skills. > Read `PRD.md` first — it is the source of truth for scope, decisions, and > rationale. This file is the build map. ## Core distinction (do not conflate) - **Stale** = the doc is *wrong* (contradicted, orphaned, superseded, provisional, completed-in-place, duplicated). Remedy: fix or remove. - **Bloat** = the doc is *true but mostly irrelevant* (distill, split, freeze). Remedy: change its altitude, almost never delete history. Severity scales with **injection frequency** — a stale line in `CLAUDE.md` (auto -injected every session) is worse than the same line in a doc nobody opens. ## Design invariants (see PRD + reversion-protection pattern) 1. The `SessionStart` hook **only reminds** — it never runs analysis or mutates anything, and spends no AI tokens. All mutation is user-invoked. 2. Reminder **snoozes** (≤ once/day while stale) via `last_reminded`. 3. State lives **in-project** under gitignored `.dochygiene/`. No global index. 4. **Report rollover**: keep only the latest report. The tool must not become the bloat it polices. 5. Cleanup is **git-safe**: clean/committed tree required (or auto WIP checkpoint), each run is one reviewable commit. 6. **Deterministic-first**: scan/state/patch-apply/token-estimate are scripts (no model). AI does only classification and prose distillation. 7. **Safety tiers**: `auto` (deterministic+reversible+objective) runs without prompt; `confirm` (destructive/subjective/generative) escalates for approval. 8. **mtime guard**: never apply a cached edit to a file changed since the check. 9. Frozen/ignored files (`hygiene: frozen` frontmatter, `.dochygiene-ignore`, append-only logs) are never flagged. Changing any invariant requires updating `invariants.md` and the golden examples, with explicit human approval. ## Build order (report schema gates everything) 1. **Machine report schema** — the contract every component consumes. Freeze first. (per-file: category, signals, op, op-type, safety tier, optional exact-edit, token estimate.) 2. **Deterministic core** — *build-spike #1 first*: a trivial `hooks/hooks.json` emitting a `systemMessage` banner on `SessionStart`, confirmed to render visibly in a real session (no plugin in this collection wires a CC hook yet). Then: scanner (signals + shortlist), state store (timestamps, rollover, snooze, atomic writes, project-root resolution), reminder hook (`SessionStart`, `matcher: startup|resume`, injected-clock testable). 3. **`check` skill** — scanner shortlist → AI classify (Sonnet, judgment only) → **`report_builder.py`** deterministic finalize pass → human + machine reports → update `last_check`. The finalize pass sits **between** model classification and the report write (invariants #6, #10): it computes `expected_sha256`, looks up `is_destructive`/`is_reversible` from `exact_edit.kind`, derives `safety_tier` via `validate_report.py`'s single-source `derive_safety_tier(...)`, sources `raw_tokens` from the token estimator, and assembles the schema-valid machine report + human-report skeleton. The model authors none of those four fields. 4. **`clean` skill + patch-applier** — consume report, mtime-guard, apply `deterministic` ops mechanically, delegate `generative` to Sonnet, gate `confirm`, git checkpoint + single commit, scopable by category/file → update `last_clean`. Then **`sweep`** = check-then-clean. - **RESOLVED (Phase 4):** `insert-frontmatter` ops have `has_anchor=False` and carry **no** `expected_sha256`, so invariant #8's content hash cannot protect them. The applier re-derives frontmatter freshness at apply time: re-read the file, parse frontmatter; key present with target value → idempotent no-op; key present with different value → skip (`frontmatter-key-conflict`); key absent → insert. No cached hash is trusted. This resolves the deferred hole noted in `add-check`. 5. **Bonus (v2)** — deterministic patch-apply hardening + token estimator (local tokenizer, injection-frequency weighting, bottom-up rollup). ## Intended layout (per cc-plugins conventions) ``` doc-hygiene/ ├── .claude-plugin/plugin.json # done ├── PRD.md # done — source of truth ├── CLAUDE.md # this file ├── invariants.md # TODO — declare behavioral invariants ├── skills/ # slash-command entry points: /os-doc-hygiene: │ ├── check/SKILL.md │ ├── clean/SKILL.md │ ├── status/SKILL.md │ └── sweep/SKILL.md ├── scripts/ # Python OOP — scanner, state, reminder, estimator, report_builder, applier │ └── ... ├── hooks/hooks.json # SessionStart wiring → reminder script (systemMessage banner) ├── examples/golden/ # classifier golden examples (input tree → expected report) └── tests/ # unit tests per deterministic seam + fixture doc trees ``` ## Conventions (inherited from cc-plugins) - **Language**: Python, OOP — small single-responsibility classes, dependency injection, immutable transforms where possible. See `../cc-architect/references/tool-patterns/deterministic-scripting.md`. - **Scripts**: structured JSON output, correct exit codes, idempotent, testable in isolation with injected clock/filesystem. - **Model routing**: scripts = no model; classification = Sonnet (hard cases → Opus); generative distillation = Sonnet (NOT Haiku); orchestration = Opus. - **Reversion protection**: `invariants.md` + `examples/golden/` + change-impact analysis + human gate for golden-example changes. See `../cc-architect/references/tool-patterns/reversion-protection.md`. - Read a directory's `CONTEXT.md` before its source files (progressive disclosure); generate one per directory as it's built. ## Completion steps (do NOT do until functional) 1. Add `invariants.md` and 3–5 golden examples. 2. Register in `../.claude-plugin/marketplace.json` (name + source + description, matching `plugin.json`). See `../.claude/references/plugin-registration.md`. 3. Verify installable via `/install doc-hygiene@cc-plugins`.