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` **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 - 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. - 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. - 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` **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. - 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 ├── PRD.md # done — source of truth
├── CLAUDE.md # this file ├── CLAUDE.md # this file
├── invariants.md # TODO — declare behavioral invariants ├── invariants.md # TODO — declare behavioral invariants
├── commands/ # /hygiene entry points (check / clean / sweep / status) ├── skills/ # slash-command entry points: /os-doc-hygiene:<skill>
├── skills/ │ ├── check/SKILL.md
│ ├── hygiene-check/SKILL.md │ ├── clean/SKILL.md
│ └── hygiene-clean/SKILL.md │ ├── status/SKILL.md
│ └── sweep/SKILL.md
├── scripts/ # Python OOP — scanner, state, reminder, estimator, report_builder, applier ├── scripts/ # Python OOP — scanner, state, reminder, estimator, report_builder, applier
│ └── ... │ └── ...
├── hooks/hooks.json # SessionStart wiring → reminder script (systemMessage banner) ├── 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: **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. - **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.) - **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: For each case directory:
1. Run an actual classification over `input/` — either `/hygiene check 1. Run an actual classification over `input/` — either `/os-doc-hygiene:check
--scope <case>/input` or by invoking the `hygiene-check` skill against that --scope <case>/input` or by invoking the `check` skill against that
tree. This produces a fresh machine report. tree. This produces a fresh machine report.
> **Confirm the invocation once the check command lands.** `--scope` is a > **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 > 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* > classify a case in isolation you want the check *rooted at*
> `<case>/input` (as the hermetic suite roots the scanner). Verify the exact > `<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`, 2. Diff the produced classification's **stable** fields against `expected.json`,
per entry: per entry:
- `category.class` - `category.class`

View File

@ -6,26 +6,26 @@ TBD - created by archiving change add-check. Update Purpose after archive.
### Requirement: `/hygiene` Command Surface ### Requirement: `/hygiene` Command Surface
The plugin SHALL provide a single `/hygiene` command that dispatches on its The plugin SHALL provide a single `/hygiene` command that dispatches on its
arguments. `/hygiene check [--scope <glob-or-path>] [--category <class|subtype>]` arguments. `/os-doc-hygiene:check [--scope <glob-or-path>] [--category <class|subtype>]`
SHALL invoke the `hygiene-check` skill. `/hygiene status` SHALL read and report the 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 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 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. arguments or unknown arguments SHALL print usage plus the current status.
#### Scenario: Check dispatches to the skill #### Scenario: Check dispatches to the skill
- **WHEN** the user runs `/hygiene check` - **WHEN** the user runs `/os-doc-hygiene:check`
- **THEN** the command invokes the `hygiene-check` skill, passing through any `--scope` or `--category` flag - **THEN** the command invokes the `check` skill, passing through any `--scope` or `--category` flag
#### Scenario: Status is read-only #### 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 - **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 #### 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 - **THEN** the command reports that the subcommand is not yet implemented (Phase 4) and does not mutate anything
#### Scenario: Unknown arguments print usage #### 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 ### 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 scanner, dispatch a Sonnet subagent for judgment-only classification of the
signal-bearing candidates, run the deterministic finalize pass, validate, write 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 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 #### 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) - **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 #### 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 per-entry opt-out, visually distinguishing irreversible `delete-range` entries from
reversible entries. The approved set SHALL be all `auto` entries plus any reversible entries. The approved set SHALL be all `auto` entries plus any
user-approved `confirm` entries. The gate SHALL run identically under `sweep` — the 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 #### 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 #### Scenario: sweep does not bypass the gate
- **WHEN** the user runs /hygiene sweep and the report contains confirm-tier entries - **WHEN** the user runs /os-doc-hygiene:sweep and the report contains confirm-tier entries
- **THEN** the confirm gate runs identically to a standalone /hygiene clean - **THEN** the confirm gate runs identically to a standalone /os-doc-hygiene:clean
### Requirement: Git-Safe Single Commit ### 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 #### 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 - **THEN** the run produces exactly one git commit containing all applied edits
#### Scenario: Dirty tree gets a WIP checkpoint then one cleanup commit #### 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 - **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 #### Scenario: All-skipped produces zero commits
@ -167,8 +167,8 @@ to the commit instant, not the run-start instant.
### Requirement: Clean Skill Orchestration ### Requirement: Clean Skill Orchestration
The `hygiene-clean` skill SHALL load the current report via `StateStore.read_report` The `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 (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 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 apply a scope/category filter to entries. It SHALL partition entries into
`auto+deterministic`, `confirm+deterministic` (i.e., `delete-range`), and `auto+deterministic`, `confirm+deterministic` (i.e., `delete-range`), and
@ -183,8 +183,8 @@ SHA.
#### Scenario: No report — prompt to check first #### Scenario: No report — prompt to check first
- **WHEN** the user runs /hygiene clean and no report exists in .dochygiene/ - **WHEN** the user runs /os-doc-hygiene:clean and no report exists in .dochygiene/
- **THEN** the skill tells the user to run /hygiene check first and stops without modifying anything - **THEN** the skill tells the user to run /os-doc-hygiene:check first and stops without modifying anything
#### Scenario: Invalid report — stop #### 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 - **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 - **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 `/os-doc-hygiene:sweep` SHALL invoke the `check` skill followed by the
`hygiene-clean` skill, passing `--scope` and `--category` to both. The sweep SHALL `clean` skill, passing `--scope` and `--category` to both. The sweep SHALL
NOT produce a double-commit: `check` writes only to the gitignored `.dochygiene/` 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 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`, commit the sweep produces. The sweep routing SHALL live in its own `sweep` skill
not in a third skill or by having the clean skill invoke the check skill internally. (`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 - **WHEN** the user runs /os-doc-hygiene:sweep
- **THEN** commands/hygiene.md invokes hygiene-check then hygiene-clean sequentially, passing shared flags - **THEN** the `sweep` skill invokes the `check` skill then the `clean` skill sequentially, passing shared flags
#### Scenario: Sweep produces at most one cleanup commit #### 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 - **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 # 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 # slash command the user runs". That command (`/os-doc-hygiene:check`) is not
# this change, so per the task we advertise that monitoring is active without # 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 # 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`. # 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 User-invoked skills for the doc-hygiene plugin. Skills own the multi-step
workflows that mix deterministic scripts (invariant #6 — no model) with the workflows that mix deterministic scripts (invariant #6 — no model) with the
narrow model steps (classification, distillation). They are dispatched by the narrow model steps (classification, distillation). They are invoked directly
`/hygiene` command, never by the `SessionStart` hook (invariant #1 — the hook as slash commands (`/os-doc-hygiene:check`, `/os-doc-hygiene:clean`,
only reminds). `/os-doc-hygiene:status`, `/os-doc-hygiene:sweep`) — never by the
`SessionStart` hook (invariant #1 — the hook only reminds).
## Contents ## Contents
| Path | Purpose | | 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). | | `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. | | `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. | | `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. | | `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) ## Planned additions (future changes)

View File

@ -1,6 +1,6 @@
--- ---
name: hygiene-check 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 `/hygiene check [--scope <glob-or-path>] [--category <class|subtype>]`. 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 # Hygiene Check Skill
@ -26,7 +26,7 @@ resolved. Use the session scratchpad directory for all intermediate artifacts
## Arguments ## 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 - `--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 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" - description: "Classify doc-hygiene candidates"
- prompt: | - prompt: |
Read and follow the workflow at: 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> Project root: <scan.project_root>
@ -264,7 +264,7 @@ Reports written:
<project-root>/.dochygiene/report.json <project-root>/.dochygiene/report.json
<project-root>/.dochygiene/report.md <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 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 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, via the patch applier, delegates generative distillation to a Sonnet subagent,
produces exactly one cleanup commit, and stamps `last_clean`. produces exactly one cleanup commit, and stamps `last_clean`.

View File

@ -1,6 +1,6 @@
--- ---
name: hygiene-clean 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 `/hygiene clean [--scope <glob-or-path>] [--category <class|subtype>]`. 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 # Hygiene Clean Skill
@ -27,7 +27,7 @@ project directory (`cwd`).
## Arguments ## 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** - `--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 (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 ### Step 1 — (D) Load report via StateStore
Resolve the project root and read the current report. If no report exists, stop 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 ```bash
python3 -c ' python3 -c '
@ -67,7 +67,7 @@ else:
``` ```
- If `status == "no_report"` → tell the user: "No hygiene report found. Run - 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 - If `status == "ok"` → record `PROJECT_ROOT` and `REPORT_PATH` (the canonical
`.dochygiene/report.json`) for use in subsequent steps. `.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 `0` → valid. Proceed to Step 3.
- Exit `1` → invalid. Show the violation list from stdout and stop: "The hygiene - 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). - Exit `2` → usage error (file missing after Step 1 succeeded = internal bug).
Stop and report. 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 ### 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 > **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 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. 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>" - description: "Distill doc-hygiene generative entry: <entry.path>"
- prompt: | - prompt: |
Read and follow the workflow at: 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> File path (project-root-relative): <entry.path>
Category: <entry.category.class> / <entry.category.subtype> Category: <entry.category.class> / <entry.category.subtype>
@ -526,27 +526,27 @@ doc-hygiene clean complete (commit: <COMMIT_SHA>)
Confirmed (deterministic): <count> Confirmed (deterministic): <count>
Confirmed (generative): <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> <for each skipped: path · kind · reason>
Incompatible-ops (skipped): <count> Incompatible-ops (skipped): <count>
<for each: path reason: generative+deterministic on same file> <for each: path reason: generative+deterministic on same file>
Untracked (skipped): <count> 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: 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. flag any new broken_reference signals in files that pointed to the moved doc.
``` ```
If entries were skipped, append: 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 | | Condition | Action |
|-----------|--------| |-----------|--------|
| No report (Step 1) | Tell user to run `/hygiene check`. STOP. No mutation. | | 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 `/hygiene check`. STOP. | | 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. | | 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. | | Approved set empty (Step 5) | Report "nothing approved". STOP. No git ops. |
| Applier exit 2 (Step 7) | Rollback to baseline, abort with structured error. | | 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 ## LOOP GUARD
The generative subagent prompt MUST point to 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). recursive skill invocation).
**SUBAGENT AUTHORIZATION:** the distill subagent is the executor; it MUST NOT **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, 6. **Be conservative.** When uncertain whether to delete or keep a passage,
keep it (distilled if possible). Deletions are harder to recover than keep it (distilled if possible). Deletions are harder to recover than
over-cautious prose. 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. 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 - exact_edit.expected_sha256 == a fresh sha256 of the input/ file bytes
(proves the golden's hashes track the fixtures). (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 against input/ and diffing the produced classification) lives OUTSIDE this suite
see examples/golden/classifier/README.md. see examples/golden/classifier/README.md.