# PRD: `doc-hygiene` plugin > Status: shipped · Built at `plugins/os-doc-hygiene/` in cc-os, not the > `cc-plugins` target/marketplace named below. Historical design doc — kept > as-is except this note and the location/marketplace corrections below. ## Problem Statement When I work intensively in a project, documentation accumulates two distinct defects that both degrade how usefully an AI agent (and I) can read the repo: - **Stale docs** — content that is now *wrong*: it contradicts the running system, references files/symbols/paths that no longer exist, is pinned to an old tool version, claims "in progress" for shipped work, or duplicates a fact that has since drifted. Trusting it causes wrong actions. This is the high-severity class because agents read confidently and don't sanity-check. - **Bloated docs** — content that is still *true* but mostly irrelevant: long debugging narratives, resolved-problem detail at full fidelity, decisions recorded without distillation, append-only growth. It doesn't lie; it dilutes attention and burns context-window budget, and buries the one live caveat in archaeology. Severity scales with **injection frequency**: a stale line in a doc nobody opens is annoying; the same line in an auto-injected file (`CLAUDE.md`, memory index) misleads *every* session unprompted. Today I notice this only incidentally, have no systematic way to find it across projects, and cleaning it up by hand wastes both my time and AI tokens. I also don't want a tool that interrupts my flow or silently rewrites my repos. ## Solution A Claude Code plugin, installed globally but operating per-project, that: 1. **Reminds, doesn't nag.** A deterministic `SessionStart` hook checks an in-project timestamp and, only if the last hygiene check is older than a threshold, injects a noticeable reminder telling me how many days it's been and which command to run. Zero AI tokens unless I act. Snoozes so it reminds at most once per day while stale. 2. **Checks on demand.** A `check` skill runs a **deterministic scan** that gathers objective signals and a candidate shortlist, then an **AI classification pass** that reads candidates, categorizes them (stale vs bloat, by sub-type), assigns each a concrete operation tagged by *op-type* (`deterministic` | `generative`) and *safety tier* (`auto` | `confirm`), and estimates context-weight savings. It produces a **human report** (what to clean and why) and a **machine report** (structured ops for the cleaner to consume). 3. **Cleans, git-safely and scoped.** A `clean` skill consumes the machine report. For each op it checks an **mtime guard** (file unchanged since the check); `deterministic` ops are applied mechanically with no model; `generative` ops are delegated to Sonnet subagents. `auto`-tier ops run without fuss; `confirm`-tier ops escalate into an approval list. Cleanup runs only on a clean/committed tree (or after an auto-checkpoint) and lands as a single reviewable commit. The user can scope a run to specific categories or files. 4. **Composes.** A `sweep` entry runs check-then-clean in one go, still surfacing the report and gating `confirm`-tier ops. The design leads with deterministic code (scan, signals, state, patch-apply, token estimate) and reserves AI for the genuine judgment (classification and prose distillation). This keeps it fast, cheap, and trustworthy. ## User Stories 1. As a developer, I want a deterministic `SessionStart` reminder when a project's docs haven't been checked in over a threshold, so that I'm nudged without spending any AI tokens. 2. As a developer, I want the reminder to be visually noticeable and to name the exact command to run, so that acting on it is one step. 3. As a developer, I want the reminder to snooze (at most once/day while stale), so that it doesn't nag me every session for a week. 4. As a developer, I want hygiene state stored per-project in a gitignored dot directory, so that there is no global index to corrupt, race, or itself go stale. 5. As a developer, I want to run a check manually with a slash command, so that after a heavy context-building session I can clear what accumulated. 6. As a developer, I want the check to deterministically scan scoped files and compute objective signals (broken references, version skew, edit-recency vs git churn, location, append-only growth, archive-to-live ratio, frontmatter markers), so that the AI pass starts from facts, not a blank read. 7. As a developer, I want the AI pass to classify each candidate as stale (contradicted, orphaned, superseded, provisional, completed-in-place, duplicated) or bloat (distill, split, freeze), so that the right remedy is chosen per file. 8. As a developer, I want each recommended operation tagged with an op-type (`deterministic` | `generative`) and a safety tier (`auto` | `confirm`), so that the cleaner knows what it can apply mechanically and what needs a model or my approval. 9. As a developer, I want a human-readable report describing what should be cleaned and *why*, grouped by category, so that I can decide at a glance. 10. As a developer, I want a machine-readable report the cleaner consumes directly, so that the cleanup step doesn't re-derive the analysis. 11. As a developer, I want only the single most-recent report kept (rollover deletes prior reports), so that the tool's own artifacts don't become bloat. 12. As a developer, I want the check to estimate context-weight savings per file, summed into categories and a total (bottom-up), so that I can prioritize. 13. As a developer, I want savings honestly framed and weighted by injection frequency (auto-injected files counted as real per-session savings, on-demand docs as theoretical-max), so that the numbers aren't misleading. 14. As a developer, I want the cleaner to apply `deterministic`+`auto` ops (move-to-archive, freeze-stamp, known-target link fix, exact-dup dedupe) with no model and no prompt, so that no-brainer cleanup just happens. 15. As a developer, I want every destructive, subjective, or generative op to be `confirm`-tier and escalated for my approval, so that nothing surprising happens to my repos. 16. As a developer, I want a per-op mtime guard so that if a file changed since the check, its cached edit is skipped (and re-analysis recommended) rather than blindly applied. 17. As a developer, I want generative cleanup (distilling narrative into live constraints) delegated to a Sonnet subagent, so that distillation quality is high and doesn't drop the constraint that mattered. 18. As a developer, I want cleanup to run only on a clean/committed tree, or to auto-commit a WIP checkpoint first, so that my uncommitted work is never lost. 19. As a developer, I want each cleanup run to land as a single reviewable commit, so that I can inspect or revert the whole sweep trivially. 20. As a developer, I want to scope a cleanup to specific categories or files (e.g. "only the orphaned-reference fixes"), so that I stay in control of how much changes at once. 21. As a developer, I want a `sweep` command that runs check-then-clean, so that when I already trust the project I can do both in one step. 22. As a developer, I want `sweep` to still surface the report and gate `confirm`-tier ops, so that the convenience path isn't a foot-gun. 23. As a developer, I want a status command that reports last-check and last-clean timestamps for the current project, so that I can see where it stands without running anything. 24. As a developer, I want to protect files from being flagged via `hygiene: frozen` frontmatter or a `.dochygiene-ignore` file, so that deliberately-frozen records and append-only logs aren't repeatedly flagged. 25. As a developer, I want sensible scope defaults (markdown + known doc locations; excluding build/vendor/archive/graphify-out dirs) with per-project overrides, so that the scan targets docs and nothing irrelevant. 26. As a developer, I want the check and clean steps to update their own timestamps (`last_check`, `last_clean`) and the reminder to track `last_reminded`, so that the lifecycle is observable and the snooze works. 27. As a developer, I want the plugin to work in any project where it's enabled without per-project setup beyond enablement, so that adoption is frictionless. 28. As a developer, I want classifier behavior protected by golden examples and invariants, so that future edits to the plugin don't silently regress what counts as stale vs bloat. 29. As a developer, I want decisions (especially `confirm`-tier approvals) recorded, so that there's an audit trail of what was changed and why. 30. As a developer, I want the deterministic scripts to emit structured JSON and correct exit codes, so that they're independently testable and composable. ## Implementation Decisions **Distribution & activation** - New plugin `os-doc-hygiene` in the cc-os marketplace (built at `plugins/os-doc-hygiene/`; see migration note above). Installed globally, operates per-project. No global state index — the question of "how to key projects globally" is dissolved by per-project storage. - Marketplace registration deferred until the plugin is functional (documented as a build-completion step, not done now). **State & artifacts (in-project)** - A gitignored `.dochygiene/` directory at the project root (resolved via git root, fallback cwd) holds: - `state.json` — `last_check`, `last_clean`, `last_reminded` timestamps. - the single most-recent report (human `.md` + machine `.json`). - Rollover: each new check deletes the prior report before writing the new one. - `.dochygiene/` must not be tracked. The tool does **not** silently edit the user's `.gitignore` (that would itself be an outward mutation, against the non-intrusive premise). Instead: on first check it detects whether the dir is ignored and, if not, surfaces a one-line offer to add the entry — applied only on confirmation (or noted in the report). The scanner always self-excludes `.dochygiene/` regardless. **Components / modules** (described abstractly; see CLAUDE.md for the build map) - **State store** (deterministic): reads/writes `state.json`; atomic writes; resolves project root. - **Scanner** (deterministic): walks scoped files, computes signals, emits a candidate shortlist with attached signals. Respects scope config + ignore markers + frozen frontmatter + append-only detection. - **Reminder hook** (deterministic, `SessionStart`): reads state, compares to staleness threshold, applies once/day snooze via `last_reminded`, emits a noticeable notice. No model. - *Mechanism (confirmed):* plugins declare hooks in `hooks/hooks.json` at the plugin root (not under `.claude-plugin/`). The visible, zero-token notice is emitted via the hook's JSON `systemMessage` field (a user-facing banner) — NOT `additionalContext` (which is silent to the user). The command names the slash command to run. `matcher` is `startup|resume`. - *Snooze is load-bearing, not polish:* `SessionStart` also fires on `resume`, `clear`, and `compact`. The once/day `last_reminded` snooze is what prevents the banner re-firing after every compaction within a single working session. - *Constraints to respect:* keep `timeout` low (≤5s); exit 0 (never block the session); the script must be fast and side-effect-free beyond reading state. - **Classifier / report builder** (AI — the `check` skill): consumes the shortlist, reads candidates, assigns category + op-type + safety tier, writes exact edits for deterministic ops, and calls the token estimator. Emits human + machine reports. Updates `last_check`. - **Token estimator** (deterministic helper): counts tokens of removed/reduced spans with a local tokenizer approximation (no API call); weights by injection frequency; rolls up file → category → total. Framed as "context weight reduced," splitting injected vs on-demand. - **Cleanup executor** (the `clean` skill + deterministic patch-applier): consumes the machine report; mtime-guards each op; applies `deterministic` ops mechanically; delegates `generative` ops to Sonnet subagents; runs `auto` freely and escalates `confirm`; enforces git checkpoint + single-commit; scopable by category/file. Updates `last_clean`. - **Sweep** (composition): invokes check then clean, preserving the report surface and `confirm` gate. **Operation taxonomy** - Op-type `deterministic`: exact, reversible edits the check pre-computes (delete/move/stamp/known-link-fix/exact-dedupe). Applied with no model. - Op-type `generative`: prose transformations (distill, split) requiring a model at clean time (Sonnet). Deferred — not pre-written at check time — so a check is cheap even when no clean follows. - Safety tier `auto`: deterministic + reversible + objective → runs without prompt. - Safety tier `confirm`: destructive (any delete), subjective, or generative → escalated for approval. **Git safety (reversion-protection vocabulary)** - Cleanup requires a clean/committed working tree, or auto-commits a WIP checkpoint first. - Each cleanup run lands as one reviewable commit (the post-state). - `confirm`-tier approvals are logged to a decisions record. **Model routing** (per the marketplace convention) - Deterministic scan/state/patch-apply/token-estimate: **no model** (scripts). - Writing those scripts: **Haiku**. - Classification / report building: **Sonnet** (reading comprehension + judgment). The hardest stale-vs-bloat distinctions may escalate to **Opus**. - Generative distillation at clean time: **Sonnet** (explicitly *not* Haiku — this is the highest-judgment task and Haiku produces lossy summaries). - Orchestration: **Opus** (the calling Claude). **Language** - **Python**, OOP, small composable single-responsibility classes with dependency injection (per the deterministic-scripting reference), chosen for broad reach in a general-purpose plugin and for testability. **Report schema is the linchpin** - The machine report schema (per-file: category, signals, recommended op, op-type, safety tier, optional exact-edit, token estimate) is the contract every other component consumes. It is designed and frozen first. ## Testing Decisions **What makes a good test here:** assert external behavior at the highest deterministic seam — given an input doc tree / state file / report, the script produces the correct structured output and exit code. Do not assert internal class structure. The AI classification layer is pinned by golden examples, not unit assertions. **Seams (highest first):** - **Scanner** — input: a fixture doc tree; output: candidate shortlist JSON. Unit-tested against fixtures exercising each signal (broken ref, version skew, churn-vs-edit, location, append-only, frozen frontmatter, ignore file). - **State store** — timestamp read/write, rollover (only latest report kept), snooze logic. Unit-tested with an injected clock and temp dirs. - **Reminder hook** — given a `state.json` + injected clock + threshold, asserts notice / no-notice / snoozed. Unit-tested via the injected clock (no real time, no real session). - **Token estimator** — counts on known spans; injection-frequency weighting; bottom-up rollup. Unit-tested on fixed inputs. - **Patch-applier + mtime guard** — applies deterministic ops on fixtures; skips when fixture mtime is newer than the check timestamp. Unit-tested. - **Classifier (AI)** — golden examples (`examples/golden/`): input doc tree → expected report categorization, per the reversion-protection pattern. Run the check against each; flag mismatches for human review. **Prior art:** the `commit` and `cc-architect` plugins pair deterministic `scripts/` with skills and structured output; this plugin follows that split. The reversion-protection reference (`cc-architect/.../reversion-protection.md`) defines the golden-example + invariants approach used for the classifier. ## Out of Scope - A global cross-project dashboard / index (dissolved by per-project storage). - Exact Claude token counting via API at check time (local approximation only; exactness isn't worth the latency/cost for a motivational estimate). - Non-markdown content analysis beyond known doc locations (e.g. linting source code comments). Scope defaults to docs. - Auto-running cleanup from the `SessionEnd`/`SessionStart` hook — the hook only ever reminds; all mutation is user-invoked. - Pushing commits or any outbound/network action — cleanup is local commits only; the user pushes. - Editing files outside the resolved project root. - v2 polish: richer per-category savings visualizations, configurable op taxonomies beyond the built-in set. ## Further Notes - **Build order** (the report schema gates everything): (1) machine report schema; (2) deterministic scanner + state store + reminder hook; (3) `check` skill (scanner → AI classify → report); (4) `clean` skill (consume report, git-safe, scoped) + `sweep`; (5) bonus: deterministic patch-apply + token estimator. Patch-apply and token-estimate are explicitly v2 — they optimize a report format that must exist and be proven first. - **Why deterministic-first matters here specifically:** the check does the expensive cognition once (reading + deciding); cleanup executes. Pushing attribute-detection into the scanner and exact edits into the report means most cleanup needs no model at all, and a check is cheap even when no cleanup follows. - **False-positive trust risk:** without the frozen/ignore mechanism and append-only detection, the tool would re-flag deliberately-frozen records every week and lose the user's trust. This is treated as a correctness requirement, not a nicety. - **The tool must not become bloat:** report rollover (keep latest only) and the gitignored dot dir are deliberate guards against the plugin polluting the very repos it cleans. - **Linchpin mechanism verified:** the SessionStart `systemMessage` banner path was confirmed against the Claude Code hook docs before this PRD was finalized (no other plugin in the collection wires a CC hook, so it had no precedent). Build-spike #1 is still to stand up a trivial `hooks/hooks.json` emitting a `systemMessage` and confirm it renders visibly in a real session before building the deterministic core on top of it.