Align os-doc-hygiene skill naming and invocation with marketplace convention

Rename skills to bare verbs (hygiene-check -> check, hygiene-clean -> clean),
matching the os-vault/os-orchestration pattern. Remove the commands/hygiene.md
dispatcher in favor of two new skills, status and sweep, so all four actions
are invoked directly as /os-doc-hygiene:<skill> instead of through a custom
subcommand parser that no other plugin in the marketplace uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jared 2026-07-03 11:23:24 -04:00
parent b235e99145
commit 7ae45c7bf8
18 changed files with 139 additions and 194 deletions

View File

@ -89,8 +89,8 @@ to those two and fix the stale doc.
**Global os-doc-hygiene plugin** — `cc-os/plugins/os-doc-hygiene/` (git-tracked, 2026-07-03); symlinked into `~/.claude/plugins/os-doc-hygiene`
- Hooks: `hooks/hooks.json` → SessionStart hook (matcher: startup|resume) runs `scripts/reminder.py` via `${CLAUDE_PLUGIN_ROOT}` (5s timeout), emitting a deterministic zero-token reminder banner
- Behavior: Monitors stale and bloated project documentation per-project under `.dochygiene/` state dir (gitignored). SessionStart reminder is deterministic (no AI tokens, once/day snooze). Skills: `hygiene-check` (AI-assisted classification of staleness signals, emits machine+human reports), `hygiene-clean` (AI-assisted or deterministic patch application with git-safe scoped cleanup). Reversion-protected via invariants.md + golden-example test fixtures.
- Migration: migrated from standalone repo (`~/dev/cc-plugins/doc-hygiene/`, 2026-07-03) and integrated into cc-os. No content conflicts detected (doc-hygiene scope does not overlap cc-os memory system). Renamed from `doc-hygiene` to `os-doc-hygiene` per cc-os plugin naming convention.
- Behavior: Monitors stale and bloated project documentation per-project under `.dochygiene/` state dir (gitignored). SessionStart reminder is deterministic (no AI tokens, once/day snooze). Skills (verb-first, no `commands/` dispatcher — invoked directly as `/os-doc-hygiene:<skill>`, per [[cc-os-plugin-skill-naming-convention]]): `check` (AI-assisted classification of staleness signals, emits machine+human reports), `clean` (AI-assisted or deterministic patch application with git-safe scoped cleanup), `status` (read-only lifecycle-timestamp read), `sweep` (check then clean in sequence). Reversion-protected via invariants.md + golden-example test fixtures.
- Migration: migrated from standalone repo (`~/dev/cc-plugins/doc-hygiene/`, 2026-07-03) and integrated into cc-os. No content conflicts detected (doc-hygiene scope does not overlap cc-os memory system). Renamed from `doc-hygiene` to `os-doc-hygiene` per cc-os plugin naming convention. Skills renamed `hygiene-check`/`hygiene-clean` → `check`/`clean`, and the `commands/hygiene.md` dispatcher removed in favor of two new skills (`status`, `sweep`), aligning with the `os-vault`/`os-orchestration` pattern of no `commands/` directory (2026-07-03).
**Graphify** — v0.8.31 at `/home/jared/.local/bin/graphify`
- Vault graph: `~/Documents/SecondBrain/graphify-out/` — disposable, structural index rebuilt by SessionStart hook; handles relational/graph queries. Distinct from vault-index.json (planned, Plan B Phase 1): a flat `{tag: [{path, title, summary}]}` lookup rebuilt at SessionEnd, queried by the `/memory-find` skill for fast tag-based SB note discovery.

View File

@ -79,10 +79,11 @@ doc-hygiene/
├── PRD.md # done — source of truth
├── CLAUDE.md # this file
├── invariants.md # TODO — declare behavioral invariants
├── commands/ # /hygiene entry points (check / clean / sweep / status)
├── skills/
│ ├── hygiene-check/SKILL.md
│ └── hygiene-clean/SKILL.md
├── skills/ # slash-command entry points: /os-doc-hygiene:<skill>
│ ├── 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)

View File

@ -24,7 +24,7 @@ Task 1.2 requires a human to start a real Claude Code session and **visually con
**To confirm:** start a fresh Claude Code session (any directory; plugin is user-scoped) and look for, alongside the memsearch line:
[doc-hygiene] Documentation health reminder is active. Run /hygiene check to scan for stale or bloated docs.
[doc-hygiene] Documentation health reminder is active. Run /os-doc-hygiene:check to scan for stale or bloated docs.
- **Renders** → check task 1.2, then do task 1.3 (record the confirmed format: plugin-root `hooks/hooks.json`, top-level `hooks` key, per-matcher nested `hooks` array, `systemMessage` field, `matcher: startup|resume`, `timeout: 5`). Then build Group 4, then Group 5.
- **Blank** → the hooks.json format or loading is wrong for this build; debug BEFORE Group 4 inherits the format. (Format currently mirrors the canonical corrected shape from commit `4d595a9`, avoiding the 3 prior hook-format bugs.)

View File

@ -1,21 +0,0 @@
# commands/
Slash-command entry points for the doc-hygiene plugin. Thin dispatchers — they
parse `$ARGUMENTS`, route to a skill or run a read-only state read, and surface
the result. They own no analysis logic of their own (invariant #6 lives in the
scripts/skills they call).
## Contents
| File | Purpose |
|------|---------|
| `hygiene.md` | `/hygiene` — the single user entry point. Parses the first token of `$ARGUMENTS` as the subcommand and passes the rest through. Subcommands: **`check [--scope <glob-or-path>] [--category <class\|subtype>]`** invokes the `hygiene-check` skill verbatim (the skill owns the full scan → classify → finalize → validate → write → stamp `last_check` workflow; the command does NOT scan or classify); **`status`** is read-only (no skill, no model, no scan) — calls `state_store.py` via `python3 -c` and prints the three lifecycle timestamps (`last_check`/`last_clean`/`last_reminded`) plus report presence; **`clean`** / **`sweep`** are reserved and respond "not yet implemented (Phase 4)". No args or an unrecognized subcommand prints usage then runs the `status` read. Scripts resolve under `${CLAUDE_PLUGIN_ROOT}/scripts/` (mirrors `hooks/hooks.json`). |
## Arg semantics (pass-through to `hygiene-check`)
- `--scope <glob-or-path>` — narrows the scan. A glob (contains `*`) maps to the
scanner's `--globs`; a bare path is handled by the skill as a post-scan prefix
filter (the scanner's glob matcher is unreliable for mid-pattern `**`).
- `--category <class|subtype>` — filters which **entries** are produced, applied
by the skill *after* classification (the scanner is category-agnostic). `class`
∈ { `stale`, `bloat` }; `subtype` is one of the closed enum values.

View File

@ -1,103 +0,0 @@
---
name: hygiene
description: Manage stale and bloated docs. `/hygiene check` scans the project for stale/bloated documentation and writes a report; `/hygiene clean` applies documented findings; `/hygiene sweep` runs check then clean; `/hygiene status` shows the last check/clean/reminded timestamps and whether a report exists.
argument-hint: "[check [--scope <glob-or-path>] [--category <class|subtype>] | status | clean [--scope <glob-or-path>] [--category <class|subtype>] | sweep [--scope <glob-or-path>] [--category <class|subtype>]]"
---
# /hygiene — Documentation Hygiene
Single entry point for the doc-hygiene plugin. Dispatch on `$ARGUMENTS`.
Parse the first token of `$ARGUMENTS` as the subcommand; everything after it is
passed through. Scripts live under `${CLAUDE_PLUGIN_ROOT}/scripts/` (mirrors
`hooks/hooks.json`).
## `check [--scope <glob-or-path>] [--category <class|subtype>]`
Invoke the **`hygiene-check`** skill (Skill tool, `skill: "hygiene-check"`),
passing the `--scope` and `--category` arguments through verbatim. The skill owns
the full workflow — scan → classify → finalize → validate → write report → stamp
`last_check`. Do not scan or classify here; just route to the skill and surface
its summary.
## `status`
Read-only. No skill, no model, no scan. Run the state store directly and report
the three lifecycle timestamps plus whether a report exists. `state_store.py` is a
module (no CLI), so call it via `python3 -c`:
```bash
python3 -c '
import sys, os
sys.path.insert(0, os.environ["CLAUDE_PLUGIN_ROOT"] + "/scripts")
from state_store import StateStore, resolve_project_root
from pathlib import Path
s = StateStore(resolve_project_root(Path(os.getcwd())))
def fmt(dt): return dt.isoformat() if dt else "never"
print("last_check: " + fmt(s.get_last_check()))
print("last_clean: " + fmt(s.get_last_clean()))
print("last_reminded: " + fmt(s.get_last_reminded()))
print("report: " + ("present" if s.read_report() else "none"))
'
```
Present the output as a short human summary, for example:
```
doc-hygiene status for: <project-root>
last check 2026-06-20T14:03:11+00:00
last clean never
last reminded 2026-06-24T09:00:00+00:00
report present (run /hygiene check to refresh)
```
Do not classify or distill anything — these are timestamp reads only.
## `clean [--scope <glob-or-path>] [--category <class|subtype>]`
Invoke the **`hygiene-clean`** skill (Skill tool, `skill: "hygiene-clean"`),
passing the `--scope` and `--category` arguments through verbatim. The skill owns
the full workflow — load report → gate confirm-tier entries → apply deterministic
ops → dispatch generative ops → commit → stamp `last_clean`. Do not apply any
edits here; just route to the skill and surface its summary.
A report must exist before running `clean`. If the user has not yet run
`/hygiene check`, the skill will stop and instruct them to do so.
## `sweep [--scope <glob-or-path>] [--category <class|subtype>]`
`sweep` is check-then-clean: a convenience that runs both skills in sequence,
passing the same `--scope` and `--category` through to each.
1. **First**, invoke the **`hygiene-check`** skill (Skill tool,
`skill: "hygiene-check"`), passing `--scope` and `--category` verbatim.
Surface the check summary to the user.
2. **Then**, invoke the **`hygiene-clean`** skill (Skill tool,
`skill: "hygiene-clean"`), passing the same `--scope` and `--category`
verbatim.
The confirm gate in `hygiene-clean` applies identically under `sweep` — sweep
does **not** auto-approve any confirm-tier or generative entries. The user will
still be prompted to approve any entries that require it before any file
mutation occurs (invariant #7).
`sweep` produces at most one cleanup commit (the clean step); the check step
writes only to the gitignored `.dochygiene/` and does not commit.
## No arguments or unrecognized subcommand
Print usage, then run the `status` block above and show current state:
```
/hygiene — documentation hygiene
check [--scope <glob-or-path>] [--category <class|subtype>]
scan for stale/bloated docs and write a report
status show last check/clean/reminded timestamps + report presence
clean [--scope <glob-or-path>] [--category <class|subtype>]
apply documented findings (requires a report; run check first)
sweep [--scope <glob-or-path>] [--category <class|subtype>]
check then clean in sequence (confirm gate still applies)
```

View File

@ -57,15 +57,15 @@ you want to confirm the classifier still reaches the expected judgment.
For each case directory:
1. Run an actual classification over `input/` — either `/hygiene check
--scope <case>/input` or by invoking the `hygiene-check` skill against that
1. Run an actual classification over `input/` — either `/os-doc-hygiene:check
--scope <case>/input` or by invoking the `check` skill against that
tree. This produces a fresh machine report.
> **Confirm the invocation once the check command lands.** `--scope` is a
> filter on a scan rooted at the resolved project root, not a re-root — to
> classify a case in isolation you want the check *rooted at*
> `<case>/input` (as the hermetic suite roots the scanner). Verify the exact
> flag/rooting against the shipped `commands/hygiene.md` before relying on it.
> flag/rooting against the shipped `skills/check/SKILL.md` before relying on it.
2. Diff the produced classification's **stable** fields against `expected.json`,
per entry:
- `category.class`

View File

@ -6,26 +6,26 @@ TBD - created by archiving change add-check. Update Purpose after archive.
### Requirement: `/hygiene` Command Surface
The plugin SHALL provide a single `/hygiene` command that dispatches on its
arguments. `/hygiene check [--scope <glob-or-path>] [--category <class|subtype>]`
SHALL invoke the `hygiene-check` skill. `/hygiene status` SHALL read and report the
arguments. `/os-doc-hygiene:check [--scope <glob-or-path>] [--category <class|subtype>]`
SHALL invoke the `check` skill. `/os-doc-hygiene:status` SHALL read and report the
lifecycle timestamps (`last_check`, `last_clean`, `last_reminded`) and whether a
report exists, using no scan and no model. `/hygiene clean` and `/hygiene sweep`
report exists, using no scan and no model. `/os-doc-hygiene:clean` and `/os-doc-hygiene:sweep`
SHALL be reserved and SHALL report that they are not yet implemented (Phase 4). No
arguments or unknown arguments SHALL print usage plus the current status.
#### Scenario: Check dispatches to the skill
- **WHEN** the user runs `/hygiene check`
- **THEN** the command invokes the `hygiene-check` skill, passing through any `--scope` or `--category` flag
- **WHEN** the user runs `/os-doc-hygiene:check`
- **THEN** the command invokes the `check` skill, passing through any `--scope` or `--category` flag
#### Scenario: Status is read-only
- **WHEN** the user runs `/hygiene status`
- **WHEN** the user runs `/os-doc-hygiene:status`
- **THEN** the command reports `last_check`, `last_clean`, `last_reminded`, and whether a report exists, without running a scan or any model
#### Scenario: Clean and sweep are reserved
- **WHEN** the user runs `/hygiene clean` or `/hygiene sweep`
- **WHEN** the user runs `/os-doc-hygiene:clean` or `/os-doc-hygiene:sweep`
- **THEN** the command reports that the subcommand is not yet implemented (Phase 4) and does not mutate anything
#### Scenario: Unknown arguments print usage
@ -35,7 +35,7 @@ arguments or unknown arguments SHALL print usage plus the current status.
### Requirement: Check Skill Orchestrates Scan, Classification, and Report Writing
The `hygiene-check` skill SHALL orchestrate the check pipeline: run the deterministic
The `check` skill SHALL orchestrate the check pipeline: run the deterministic
scanner, dispatch a Sonnet subagent for judgment-only classification of the
signal-bearing candidates, run the deterministic finalize pass, validate, write the
report pair, and stamp `last_check`. The skill SHALL run all non-judgment steps as
@ -47,7 +47,7 @@ after classification; both SHALL be recorded in the human-report header.
#### Scenario: Skill runs the full pipeline
- **WHEN** the `hygiene-check` skill runs
- **WHEN** the `check` skill runs
- **THEN** it scans (deterministic), classifies signal-bearing candidates (Sonnet), finalizes (deterministic), validates (deterministic), writes the report pair (deterministic), and stamps `last_check` (deterministic)
#### Scenario: Zero-signal files are not read by the model

View File

@ -94,7 +94,7 @@ single batch-confirm list showing path, category, op, token count, and rationale
per-entry opt-out, visually distinguishing irreversible `delete-range` entries from
reversible entries. The approved set SHALL be all `auto` entries plus any
user-approved `confirm` entries. The gate SHALL run identically under `sweep` — the
`/hygiene sweep` convenience path SHALL NOT bypass invariant #7.
`/os-doc-hygiene:sweep` convenience path SHALL NOT bypass invariant #7.
#### Scenario: auto entries apply without prompt
@ -113,8 +113,8 @@ user-approved `confirm` entries. The gate SHALL run identically under `sweep`
#### Scenario: sweep does not bypass the gate
- **WHEN** the user runs /hygiene sweep and the report contains confirm-tier entries
- **THEN** the confirm gate runs identically to a standalone /hygiene clean
- **WHEN** the user runs /os-doc-hygiene:sweep and the report contains confirm-tier entries
- **THEN** the confirm gate runs identically to a standalone /os-doc-hygiene:clean
### Requirement: Git-Safe Single Commit
@ -137,12 +137,12 @@ to the commit instant, not the run-start instant.
#### Scenario: Clean tree produces exactly one commit
- **WHEN** the user runs /hygiene clean with a clean git tree
- **WHEN** the user runs /os-doc-hygiene:clean with a clean git tree
- **THEN** the run produces exactly one git commit containing all applied edits
#### Scenario: Dirty tree gets a WIP checkpoint then one cleanup commit
- **WHEN** the user runs /hygiene clean with unstaged changes in the working tree
- **WHEN** the user runs /os-doc-hygiene:clean with unstaged changes in the working tree
- **THEN** the skill auto-creates a WIP checkpoint commit of the user's work, then produces exactly one cleanup commit — two commits total, cleanup still exactly one
#### Scenario: All-skipped produces zero commits
@ -167,8 +167,8 @@ to the commit instant, not the run-start instant.
### Requirement: Clean Skill Orchestration
The `hygiene-clean` skill SHALL load the current report via `StateStore.read_report`
(if none, it SHALL tell the user to run `/hygiene check` and stop). It SHALL
The `clean` skill SHALL load the current report via `StateStore.read_report`
(if none, it SHALL tell the user to run `/os-doc-hygiene:check` and stop). It SHALL
re-validate the loaded report via `validate_report.py` (if invalid, stop). It SHALL
apply a scope/category filter to entries. It SHALL partition entries into
`auto+deterministic`, `confirm+deterministic` (i.e., `delete-range`), and
@ -183,8 +183,8 @@ SHA.
#### Scenario: No report — prompt to check first
- **WHEN** the user runs /hygiene clean and no report exists in .dochygiene/
- **THEN** the skill tells the user to run /hygiene check first and stops without modifying anything
- **WHEN** the user runs /os-doc-hygiene:clean and no report exists in .dochygiene/
- **THEN** the skill tells the user to run /os-doc-hygiene:check first and stops without modifying anything
#### Scenario: Invalid report — stop
@ -228,22 +228,22 @@ be treated as `incompatible-ops-on-file` and skipped.
- **WHEN** a file has both a generative entry and a deterministic entry
- **THEN** the applier (or skill) skips the file with reason incompatible-ops-on-file and recommends re-analysis
### Requirement: Sweep Composes Check Then Clean at Command Level
### Requirement: Sweep Composes Check Then Clean
`/hygiene sweep` SHALL invoke the `hygiene-check` skill followed by the
`hygiene-clean` skill, passing `--scope` and `--category` to both. The sweep SHALL
`/os-doc-hygiene:sweep` SHALL invoke the `check` skill followed by the
`clean` skill, passing `--scope` and `--category` to both. The sweep SHALL
NOT produce a double-commit: `check` writes only to the gitignored `.dochygiene/`
directory and never commits; the single cleanup commit from `clean` is the only git
commit the sweep produces. The sweep routing SHALL live in `commands/hygiene.md`,
not in a third skill or by having the clean skill invoke the check skill internally.
commit the sweep produces. The sweep routing SHALL live in its own `sweep` skill
(`skills/sweep/SKILL.md`), not by having the `clean` skill invoke `check` internally.
#### Scenario: Sweep routes through the command, not a third skill
#### Scenario: Sweep routes through its own skill, not by nesting clean inside check
- **WHEN** the user runs /hygiene sweep
- **THEN** commands/hygiene.md invokes hygiene-check then hygiene-clean sequentially, passing shared flags
- **WHEN** the user runs /os-doc-hygiene:sweep
- **THEN** the `sweep` skill invokes the `check` skill then the `clean` skill sequentially, passing shared flags
#### Scenario: Sweep produces at most one cleanup commit
- **WHEN** the user runs /hygiene sweep on a clean tree
- **WHEN** the user runs /os-doc-hygiene:sweep on a clean tree
- **THEN** at most one git commit is produced (the cleanup commit from clean); check writes nothing to git

View File

@ -137,8 +137,8 @@ class Reminder:
# ---------------------------------------------------------------------------
#
# DEVIATION NOTE: the `session-reminder` spec requires the banner to "name the
# slash command the user runs". That command (`/hygiene check`) is not built in
# this change, so per the task we advertise that monitoring is active without
# slash command the user runs". That command (`/os-doc-hygiene:check`) is not
# built in this change, so per the task we advertise that monitoring is active without
# promising an unbuilt command. The staleness phrase IS retained (the spec also
# says the banner states "how stale the docs are"), keyed off `last_check`.

View File

@ -2,17 +2,20 @@
User-invoked skills for the doc-hygiene plugin. Skills own the multi-step
workflows that mix deterministic scripts (invariant #6 — no model) with the
narrow model steps (classification, distillation). They are dispatched by the
`/hygiene` command, never by the `SessionStart` hook (invariant #1 — the hook
only reminds).
narrow model steps (classification, distillation). They are invoked directly
as slash commands (`/os-doc-hygiene:check`, `/os-doc-hygiene:clean`,
`/os-doc-hygiene:status`, `/os-doc-hygiene:sweep`) — never by the
`SessionStart` hook (invariant #1 — the hook only reminds).
## Contents
| Path | Purpose |
|------|---------|
| `hygiene-check/SKILL.md` | The `check` orchestration: **scan → classify → finalize → validate → write → stamp**. (D) `scanner.py` produces the signal-bearing shortlist; candidates = the keys of `signals` (shortlist paths with zero signals are presumptively cleared, never read by the model). (M) ONE **Sonnet** subagent classifies all candidates via `workflows/classify-candidates.md`, returning slim per-file proposals (judgment only); single-file **Opus** escalation on low-confidence hard distinctions (stale-vs-bloat, delete-vs-rewrite). (logic) `--category` filter applied at the entry stage *after* classification. (D) `report_builder.py` finalizes — fills the four guardrail fields the model must not author (`expected_sha256`, `safety_tier`, `is_destructive`/`is_reversible`, `raw_tokens`) and emits a schema-valid machine report + human-report skeleton. (D) `validate_report.py` validates **on the scratch path BEFORE writing** (write_report is destructive-first; invariant #4). (D) `StateStore.write_report` does the rollover write and `set_last_check` stamps `last_check` = the validated report's own `generated_at`. All intermediates live in a session scratch dir; nothing touches `.dochygiene/` until the validated write. **LOOP GUARD:** the subagent prompt points to `workflows/classify-candidates.md`, NEVER to `SKILL.md`. **SUBAGENT AUTHORIZATION:** every subagent dispatch includes an explicit terminal-authorization directive (REPORT-AND-EXIT, never block). |
| `hygiene-check/workflows/classify-candidates.md` | Self-contained subagent workflow for the classification step. Defines the slim proposal object (`path`, `category` {class, subtype}, pass-through `signals`, `op`, `op_type`, optional `exact_edit`/`reducible_range`, `gloss`, `confidence`, `escalate`), the closed enums, the situation→kind→tier decision table, and the per-kind required `exact_edit` fields. The subagent reads each file and returns judgment only — it never authors hashes, token counts, safety tiers, or reversibility. |
| `hygiene-clean/SKILL.md` | The `clean` orchestration: **load → validate → filter → gate → preflight → apply-deterministic → apply-generative → commit → stamp**. (D) `StateStore` loads and re-validates the machine report. (logic) Incompatible-pair detection (files with both generative + deterministic ops) and partition by `safety_tier`/`op_type`. (M-GATE) Confirm gate for `confirm`-tier entries; auto-tier entries apply silently. (D) Git preflight: record baseline, detect untracked files (which get skipped). (D) `patch_applier.py` applies deterministic entries mechanically; mtime-guarded, output include `applied`/`skipped`/`failed`. (M) Sonnet subagent distills each generative entry via `workflows/distill.md` (LOOP-GUARD + SUBAGENT AUTHORIZATION — terminal, REPORT-AND-EXIT). (D) `git-context commit-apply` produces exactly one cleanup commit. (D) `StateStore.set_last_clean` stamps the commit instant. Scoped by `--scope` (glob-or-path) and `--category` (class or subtype), both entry-stage filters. On failure (rollback-worthy): all intermediate mutations reverted to baseline. |
| `hygiene-clean/workflows/distill.md` | Self-contained subagent workflow for generative distillation. Receives live file contents (not disk-read), category, op, and signals. Performs `distill` (condense in place, preserve frontmatter and structure, ~4060% target length), `split` (extract to archive + pointer, archive path under `archive/`), or prose-rewrite ops (`provisional`, `contradicted`). Returns structured output (`DISTILL_RESULT_START`/`END` for single-file; `SPLIT_PRIMARY_START`/`END` + `SPLIT_ARCHIVE_DEST` + `SPLIT_ARCHIVE_START`/`END` for split). Never writes files or calls git; never re-reads disk. |
| `check/SKILL.md` | The `check` orchestration: **scan → classify → finalize → validate → write → stamp**. (D) `scanner.py` produces the signal-bearing shortlist; candidates = the keys of `signals` (shortlist paths with zero signals are presumptively cleared, never read by the model). (M) ONE **Sonnet** subagent classifies all candidates via `workflows/classify-candidates.md`, returning slim per-file proposals (judgment only); single-file **Opus** escalation on low-confidence hard distinctions (stale-vs-bloat, delete-vs-rewrite). (logic) `--category` filter applied at the entry stage *after* classification. (D) `report_builder.py` finalizes — fills the four guardrail fields the model must not author (`expected_sha256`, `safety_tier`, `is_destructive`/`is_reversible`, `raw_tokens`) and emits a schema-valid machine report + human-report skeleton. (D) `validate_report.py` validates **on the scratch path BEFORE writing** (write_report is destructive-first; invariant #4). (D) `StateStore.write_report` does the rollover write and `set_last_check` stamps `last_check` = the validated report's own `generated_at`. All intermediates live in a session scratch dir; nothing touches `.dochygiene/` until the validated write. **LOOP GUARD:** the subagent prompt points to `workflows/classify-candidates.md`, NEVER to `SKILL.md`. **SUBAGENT AUTHORIZATION:** every subagent dispatch includes an explicit terminal-authorization directive (REPORT-AND-EXIT, never block). |
| `check/workflows/classify-candidates.md` | Self-contained subagent workflow for the classification step. Defines the slim proposal object (`path`, `category` {class, subtype}, pass-through `signals`, `op`, `op_type`, optional `exact_edit`/`reducible_range`, `gloss`, `confidence`, `escalate`), the closed enums, the situation→kind→tier decision table, and the per-kind required `exact_edit` fields. The subagent reads each file and returns judgment only — it never authors hashes, token counts, safety tiers, or reversibility. |
| `clean/SKILL.md` | The `clean` orchestration: **load → validate → filter → gate → preflight → apply-deterministic → apply-generative → commit → stamp**. (D) `StateStore` loads and re-validates the machine report. (logic) Incompatible-pair detection (files with both generative + deterministic ops) and partition by `safety_tier`/`op_type`. (M-GATE) Confirm gate for `confirm`-tier entries; auto-tier entries apply silently. (D) Git preflight: record baseline, detect untracked files (which get skipped). (D) `patch_applier.py` applies deterministic entries mechanically; mtime-guarded, output include `applied`/`skipped`/`failed`. (M) Sonnet subagent distills each generative entry via `workflows/distill.md` (LOOP-GUARD + SUBAGENT AUTHORIZATION — terminal, REPORT-AND-EXIT). (D) `git-context commit-apply` produces exactly one cleanup commit. (D) `StateStore.set_last_clean` stamps the commit instant. Scoped by `--scope` (glob-or-path) and `--category` (class or subtype), both entry-stage filters. On failure (rollback-worthy): all intermediate mutations reverted to baseline. |
| `clean/workflows/distill.md` | Self-contained subagent workflow for generative distillation. Receives live file contents (not disk-read), category, op, and signals. Performs `distill` (condense in place, preserve frontmatter and structure, ~4060% target length), `split` (extract to archive + pointer, archive path under `archive/`), or prose-rewrite ops (`provisional`, `contradicted`). Returns structured output (`DISTILL_RESULT_START`/`END` for single-file; `SPLIT_PRIMARY_START`/`END` + `SPLIT_ARCHIVE_DEST` + `SPLIT_ARCHIVE_START`/`END` for split). Never writes files or calls git; never re-reads disk. |
| `status/SKILL.md` | Read-only lifecycle-timestamp read (no model, no scan): `last_check`/`last_clean`/`last_reminded` + report presence, via `state_store.py`. |
| `sweep/SKILL.md` | Convenience wrapper: invokes `check` then `clean` in sequence, passing `--scope`/`--category` through to both. The confirm gate in `clean` still applies unchanged. |
## Planned additions (future changes)

View File

@ -1,6 +1,6 @@
---
name: hygiene-check
description: Scan the project for stale and bloated documentation and write a hygiene report. Runs the deterministic scanner, dispatches a Sonnet subagent to classify only the signal-bearing candidates, finalizes/validates the machine report deterministically, then writes the report pair and stamps `last_check`. Invoked by `/hygiene check [--scope <glob-or-path>] [--category <class|subtype>]`.
name: check
description: Scan the project for stale and bloated documentation and write a hygiene report. Runs the deterministic scanner, dispatches a Sonnet subagent to classify only the signal-bearing candidates, finalizes/validates the machine report deterministically, then writes the report pair and stamps `last_check`. Invoked by `/os-doc-hygiene:check [--scope <glob-or-path>] [--category <class|subtype>]`.
---
# Hygiene Check Skill
@ -26,7 +26,7 @@ resolved. Use the session scratchpad directory for all intermediate artifacts
## Arguments
Passed through from `/hygiene check`:
Passed through from `/os-doc-hygiene:check`:
- `--scope <glob-or-path>` — narrow the scan. A glob (contains `*`) maps to the
scanner's `--globs`. A **bare path** does NOT map cleanly to a `--globs` value
@ -120,7 +120,7 @@ Agent tool parameters:
- description: "Classify doc-hygiene candidates"
- prompt: |
Read and follow the workflow at:
${CLAUDE_PLUGIN_ROOT}/skills/hygiene-check/workflows/classify-candidates.md
${CLAUDE_PLUGIN_ROOT}/skills/check/workflows/classify-candidates.md
Project root: <scan.project_root>
@ -264,7 +264,7 @@ Reports written:
<project-root>/.dochygiene/report.json
<project-root>/.dochygiene/report.md
Run /hygiene clean to act on these (Phase 4), or /hygiene status for timestamps.
Run /os-doc-hygiene:clean to act on these (Phase 4), or /os-doc-hygiene:status for timestamps.
```
The human report header renders `scope_globs` but has no category field (the

View File

@ -1,7 +1,7 @@
# hygiene-clean/
# clean/
The `clean` skill: documentation cleanup orchestrator. Loads the machine report
from `/hygiene check`, gates confirm-tier entries, applies deterministic ops
from `/os-doc-hygiene:check`, gates confirm-tier entries, applies deterministic ops
via the patch applier, delegates generative distillation to a Sonnet subagent,
produces exactly one cleanup commit, and stamps `last_clean`.

View File

@ -1,6 +1,6 @@
---
name: hygiene-clean
description: Apply documented hygiene findings to project docs. Loads the current machine report, gates confirm-tier entries, applies deterministic ops via the patch applier, dispatches generative ops to a Sonnet subagent, stages precisely, and produces exactly one git commit. Invoked by `/hygiene clean [--scope <glob-or-path>] [--category <class|subtype>]`.
name: clean
description: Apply documented hygiene findings to project docs. Loads the current machine report, gates confirm-tier entries, applies deterministic ops via the patch applier, dispatches generative ops to a Sonnet subagent, stages precisely, and produces exactly one git commit. Invoked by `/os-doc-hygiene:clean [--scope <glob-or-path>] [--category <class|subtype>]`.
---
# Hygiene Clean Skill
@ -27,7 +27,7 @@ project directory (`cwd`).
## Arguments
Passed through from `/hygiene clean`:
Passed through from `/os-doc-hygiene:clean`:
- `--scope <glob-or-path>` — narrow which entries to act on. Applied at **Step 3**
(entry stage), same semantics as the check skill: a glob (contains `*`) matches
@ -46,7 +46,7 @@ If no arguments are given, act on all entries in the report.
### Step 1 — (D) Load report via StateStore
Resolve the project root and read the current report. If no report exists, stop
immediately and tell the user to run `/hygiene check` first.
immediately and tell the user to run `/os-doc-hygiene:check` first.
```bash
python3 -c '
@ -67,7 +67,7 @@ else:
```
- If `status == "no_report"` → tell the user: "No hygiene report found. Run
`/hygiene check` first to generate one." **STOP.**
`/os-doc-hygiene:check` first to generate one." **STOP.**
- If `status == "ok"` → record `PROJECT_ROOT` and `REPORT_PATH` (the canonical
`.dochygiene/report.json`) for use in subsequent steps.
@ -84,7 +84,7 @@ python3 "${CLAUDE_PLUGIN_ROOT}/scripts/validate_report.py" "$REPORT_PATH"
- Exit `0` → valid. Proceed to Step 3.
- Exit `1` → invalid. Show the violation list from stdout and stop: "The hygiene
report is invalid. Re-run `/hygiene check` to regenerate it." **STOP.**
report is invalid. Re-run `/os-doc-hygiene:check` to regenerate it." **STOP.**
- Exit `2` → usage error (file missing after Step 1 succeeded = internal bug).
Stop and report.
@ -174,7 +174,7 @@ confirm_gen = [(i, e) for i, e in in_scope_clean
### Step 5 — (M-GATE) Confirm gate — BEFORE any mutation or git op
> **Invariant #7:** this gate must run before the first file write or git
> operation. It runs identically under `/hygiene sweep`.
> operation. It runs identically under `/os-doc-hygiene:sweep`.
Only present the gate if `confirm_det` or `confirm_gen` is non-empty. If both
are empty (all entries are `auto`), skip directly to Step 6 — no prompt.
@ -371,7 +371,7 @@ For each approved generative entry (in order):
- description: "Distill doc-hygiene generative entry: <entry.path>"
- prompt: |
Read and follow the workflow at:
${CLAUDE_PLUGIN_ROOT}/skills/hygiene-clean/workflows/distill.md
${CLAUDE_PLUGIN_ROOT}/skills/clean/workflows/distill.md
File path (project-root-relative): <entry.path>
Category: <entry.category.class> / <entry.category.subtype>
@ -526,27 +526,27 @@ doc-hygiene clean complete (commit: <COMMIT_SHA>)
Confirmed (deterministic): <count>
Confirmed (generative): <count>
Skipped: <count> (re-analysis recommended — re-run /hygiene check)
Skipped: <count> (re-analysis recommended — re-run /os-doc-hygiene:check)
<for each skipped: path · kind · reason>
Incompatible-ops (skipped): <count>
<for each: path reason: generative+deterministic on same file>
Untracked (skipped): <count>
<for each: path run `git add <path>` then re-run /hygiene check>
<for each: path run `git add <path>` then re-run /os-doc-hygiene:check>
```
If a `move-to-archive` was applied, append:
```
Note: move-to-archive ops create link orphans. The next /hygiene check will
Note: move-to-archive ops create link orphans. The next /os-doc-hygiene:check will
flag any new broken_reference signals in files that pointed to the moved doc.
```
If entries were skipped, append:
```
Run /hygiene check to refresh the report and pick up any remaining issues.
Run /os-doc-hygiene:check to refresh the report and pick up any remaining issues.
```
---
@ -555,8 +555,8 @@ Run /hygiene check to refresh the report and pick up any remaining issues.
| Condition | Action |
|-----------|--------|
| No report (Step 1) | Tell user to run `/hygiene check`. STOP. No mutation. |
| Invalid report (Step 2) | Show violations. Tell user to re-run `/hygiene check`. STOP. |
| No report (Step 1) | Tell user to run `/os-doc-hygiene:check`. STOP. No mutation. |
| Invalid report (Step 2) | Show violations. Tell user to re-run `/os-doc-hygiene:check`. STOP. |
| Zero in-scope entries (Step 3) | Report no-op. STOP. No git ops. |
| Approved set empty (Step 5) | Report "nothing approved". STOP. No git ops. |
| Applier exit 2 (Step 7) | Rollback to baseline, abort with structured error. |
@ -590,7 +590,7 @@ echo "Rolled back to $(git rev-parse --short HEAD). No commit was created."
## LOOP GUARD
The generative subagent prompt MUST point to
`skills/hygiene-clean/workflows/distill.md`, NEVER to this SKILL.md (prevents
`skills/clean/workflows/distill.md`, NEVER to this SKILL.md (prevents
recursive skill invocation).
**SUBAGENT AUTHORIZATION:** the distill subagent is the executor; it MUST NOT

View File

@ -142,5 +142,5 @@ DISTILL_ERROR: <one-sentence reason why this entry cannot be processed>
6. **Be conservative.** When uncertain whether to delete or keep a passage,
keep it (distilled if possible). Deletions are harder to recover than
over-cautious prose.
7. **Do not re-invoke the parent skill** (`hygiene-clean`). This workflow is
7. **Do not re-invoke the parent skill** (`clean`). This workflow is
the final step; recursion would be incorrect.

View File

@ -0,0 +1,39 @@
---
name: status
description: Show the doc-hygiene lifecycle state for the current project — last check/clean/reminded timestamps and whether a report exists. Read-only, no model, no scan. Invoked by `/os-doc-hygiene:status`.
---
# Status Skill
Read-only. No model, no scan, no mutation. Reads the three lifecycle
timestamps plus report presence via `state_store.py` and reports them.
`state_store.py` is a module (no CLI), so call it via `python3 -c`:
```bash
python3 -c '
import sys, os
sys.path.insert(0, os.environ["CLAUDE_PLUGIN_ROOT"] + "/scripts")
from state_store import StateStore, resolve_project_root
from pathlib import Path
s = StateStore(resolve_project_root(Path(os.getcwd())))
def fmt(dt): return dt.isoformat() if dt else "never"
print("last_check: " + fmt(s.get_last_check()))
print("last_clean: " + fmt(s.get_last_clean()))
print("last_reminded: " + fmt(s.get_last_reminded()))
print("report: " + ("present" if s.read_report() else "none"))
'
```
Present the output as a short human summary, for example:
```
doc-hygiene status for: <project-root>
last check 2026-06-20T14:03:11+00:00
last clean never
last reminded 2026-06-24T09:00:00+00:00
report present (run /os-doc-hygiene:check to refresh)
```
Do not classify or distill anything — this is a timestamp read only.

View File

@ -0,0 +1,26 @@
---
name: sweep
description: Run doc-hygiene check then clean in sequence — a convenience wrapper. The confirm gate in `clean` still applies; sweep never auto-approves confirm-tier or generative entries. Invoked by `/os-doc-hygiene:sweep [--scope <glob-or-path>] [--category <class|subtype>]`.
---
# Sweep Skill
`sweep` is check-then-clean: a convenience that runs both skills in sequence,
passing the same `--scope` and `--category` through to each.
## Workflow
1. **First**, invoke the **`check`** skill (Skill tool, `skill: "check"`),
passing `--scope` and `--category` verbatim. Surface the check summary to
the user.
2. **Then**, invoke the **`clean`** skill (Skill tool, `skill: "clean"`),
passing the same `--scope` and `--category` verbatim.
The confirm gate in `clean` applies identically under `sweep` — sweep does
**not** auto-approve any confirm-tier or generative entries (invariant #7).
The user will still be prompted to approve any entries that require it before
any file mutation occurs.
`sweep` produces at most one cleanup commit (the clean step); the check step
writes only to the gitignored `.dochygiene/` and does not commit.

View File

@ -22,7 +22,7 @@ per the META-RULE). This suite asserts only the deterministic / stable parts:
- exact_edit.expected_sha256 == a fresh sha256 of the input/ file bytes
(proves the golden's hashes track the fixtures).
The LIVE model-classification regression (actually running /hygiene check
The LIVE model-classification regression (actually running /os-doc-hygiene:check
against input/ and diffing the produced classification) lives OUTSIDE this suite
see examples/golden/classifier/README.md.