# 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.
