19 KiB
doc-clean Specification
Purpose
Defines the clean skill and patch applier that consume a validated check
report and apply its findings git-safely: per-file transactional edits guarded
by content hashes, safety-tier gating that escalates destructive or generative
ops for confirmation, tracked-only true deletion under the lifecycle tier
matrix, and a single reviewable commit per run (with sweep composing check
then clean).
Requirements
Requirement: Per-File Transaction with Content-Hash Guard
The patch applier SHALL apply all entries for a given file as a single in-memory
transaction. It SHALL read the file's bytes once, verify that sha256(bytes)
matches expected_sha256 for every anchored entry on that file, and on any
mismatch skip the entire file batch with reason content-changed-since-check
(recommending re-analysis) while continuing to process other files. It SHALL apply
anchored edits in memory in descending order by anchor.start_line, and SHALL apply
insert-frontmatter last (after all anchor edits) because it prepends and shifts
line numbers. It SHALL write the file once and stage once after all in-memory edits
succeed.
Scenario: Mismatch skips the entire file batch
- WHEN a file's current sha256 differs from any anchored entry's
expected_sha256 - THEN the applier skips the entire file batch with reason
content-changed-since-check, leaves the file untouched, and continues processing other files
Scenario: Descending-anchor application preserves line offsets
- WHEN a file has multiple anchored entries (e.g., line 40 and line 10)
- THEN the applier applies the line-40 edit first, then the line-10 edit, so the second edit lands on the correct line in the still-unmodified earlier portion of the file
Scenario: insert-frontmatter is applied last
- WHEN a file has both an anchored edit and an insert-frontmatter entry
- THEN the anchored edit is applied first in memory, and insert-frontmatter is appended last (after all anchor edits) before the single file write
Scenario: Write and stage occur once per file
- WHEN a file has multiple entries applied in memory
- THEN the file is written to disk exactly once and staged exactly once after all in-memory edits succeed
Requirement: insert-frontmatter Re-Derives Freshness at Apply Time
The patch applier SHALL re-derive frontmatter state at apply time for
insert-frontmatter entries (which carry no expected_sha256 because they have no
anchor) by re-reading the file and parsing its frontmatter. If the target key is
already present with the target value, the applier SHALL treat the operation as an
idempotent no-op (success, no write). If the target key is present with a different
value, the applier SHALL skip the entry with reason frontmatter-key-conflict and
SHALL NOT overwrite the existing value. If the key is absent, the applier SHALL
insert key: value (creating a --- block if none exists) as the last in-memory
step.
Scenario: Key already present with target value — idempotent no-op
- WHEN the applier processes an insert-frontmatter entry and the file already contains
hygiene: frozen - THEN the applier treats the entry as a success and does not modify the file
Scenario: Key present with conflicting value — skip
- WHEN the applier processes an insert-frontmatter entry and the file already contains
hygiene: review - THEN the applier skips the entry with reason
frontmatter-key-conflictand does not overwrite the existing value
Scenario: Key absent — insert
- WHEN the applier processes an insert-frontmatter entry and the file has no
hygienefrontmatter key - THEN the applier inserts the key-value pair (creating a
---block if needed) as the last in-memory step before writing
Requirement: Incompatible-Ops Detection Skips the File
The patch applier SHALL detect incompatible-op combinations on a per-file basis
before performing any edits. If move-to-archive co-occurs with any other op on the
same file, or if any anchor ranges overlap, the applier SHALL skip the entire file
with reason incompatible-ops-on-file and SHALL recommend re-analysis. The applier
SHALL NOT attempt to compose or sequence incompatible ops in v1.
Scenario: move-to-archive with another op — skip
- WHEN a file has both a move-to-archive entry and a replace-text entry
- THEN the applier skips the entire file with reason
incompatible-ops-on-fileand records a re-analysis recommendation
Scenario: Overlapping anchors — skip
- WHEN two entries on the same file have anchor ranges that overlap (e.g., lines 5–15 and lines 10–20)
- THEN the applier skips the entire file with reason
incompatible-ops-on-file
Scenario: Non-conflicting entries on a file proceed normally
- WHEN a file has two entries with non-overlapping anchors and no move-to-archive
- THEN the applier applies both entries via the descending-anchor transaction
Requirement: Safety-Tier Gating Before Any Mutation
The clean skill SHALL partition report entries by safety tier (read from the
report — never recomputed) into auto entries (applied without prompt) and
confirm entries (escalated before any mutation). It SHALL present all
confirm-tier entries as a single batch-confirm list showing path, category,
op, token count, and rationale with per-entry opt-out, visually
distinguishing irreversible delete-range, delete, and
extract-then-delete 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 /os-doc-hygiene:sweep
convenience path SHALL NOT bypass invariant #7. Regardless of the tier
recorded in the report, any delete or extract-then-delete entry SHALL be
re-verified at apply time per the lifecycle-deletion tier matrix (tracked
- clean required for auto) before being applied without a prompt.
Scenario: auto entries apply without prompt
- WHEN the report contains only auto-tier entries (move-to-archive, insert-frontmatter, replace-text, dedupe)
- THEN the clean skill applies them without presenting a confirm prompt
Scenario: confirm entries escalate before any mutation
- WHEN the report contains confirm-tier entries (delete-range, delete, extract-then-delete, or any generative op)
- THEN the clean skill presents a batch-confirm list before any file is modified, and applies only user-approved entries
Scenario: per-entry opt-out is respected
- WHEN the user approves some confirm entries and opts out of others
- THEN the skill applies the approved entries and skips the opted-out entries
Scenario: sweep does not bypass the gate
- 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
Scenario: A report-tier auto delete is downgraded if runtime state has changed
- WHEN a
deleteentry was tieredautoin the report but the applier's runtime check finds the file is now dirty or untracked - THEN the applier does not apply it silently; it is skipped and reported for re-analysis, never trusted from the cached tier
Requirement: Git-Safe Single Commit
The clean skill SHALL produce exactly one git commit per run. Before any
mutation it SHALL resolve the project root via StateStore, run
git status --porcelain, and if the tree is dirty, SHALL automatically
create a WIP checkpoint commit of the user's work before proceeding, so the
cleanup commit remains exactly one. The cleanup commit SHALL be created via
git-context commit-apply --message-stdin with a generated message
summarizing auto/confirmed/skipped counts and op breakdown, including
lifecycle delete/extract-then-delete counts when present. Staging SHALL be
precise: for non-move, non-delete ops the skill calls git add <staged_paths> from the applier result; for move-to-archive the applier
calls git mv (staging both sides) and the skill SHALL NOT git add the
destination path again; for delete and the delete half of
extract-then-delete the applier calls git rm (staging the removal
itself) and the skill SHALL NOT separately stage the removed path. The skill
SHALL NEVER use git add -A or git add .. If a hard failure occurs
(applier exit 2, git mv/git rm fail, or write error), the skill SHALL
roll back via git restore/reset to the pre-run baseline and abort with a
structured error. Partial success (some file batches guard-skipped) SHALL
NOT trigger rollback — the skill SHALL commit what applied and report
skipped files. Untracked candidate docs SHALL be skipped and reported
(tracked-files-only) except where a lifecycle rule explicitly escalates an
untracked delete to confirm and the user approves it. last_clean SHALL be
stamped to the commit instant, not the run-start instant.
Scenario: Clean tree produces exactly one commit
- 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, including any lifecycle deletes
Scenario: Dirty tree gets a WIP checkpoint then one cleanup commit
- 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
- WHEN all entries are guard-skipped (every file changed since check)
- THEN the skill produces zero commits and reports all files as skipped with re-analysis recommended
Scenario: Hard failure triggers rollback
- WHEN a write error, applier exit 2, or a
git rmfailure occurs mid-run - THEN the skill rolls back to the pre-run baseline and aborts with a structured error; no partial commit is created
Scenario: Partial success commits what applied
- WHEN some file batches are guard-skipped (applier exit 1) and others succeed
- THEN the skill commits the applied edits and reports the skipped files, without rolling back the applied changes
Scenario: move-to-archive is not double-staged
- WHEN the applier stages a move-to-archive via git mv (both source and dest)
- THEN the skill does not call git add on the destination path again
Scenario: A lifecycle delete is not double-staged
- WHEN the applier stages a
deleteviagit rm - THEN the skill does not separately call
git addor any other staging command on the removed path
Requirement: Applier Applies Delete and Extract-Then-Delete Under the Tier Matrix
The patch applier SHALL support two new op kinds, delete and
extract-then-delete. For both, immediately before applying, it SHALL
re-run git ls-files <path> and a dirty check against that specific path —
never trusting the cached report tier or rule claim — and SHALL apply the
tier matrix from the lifecycle-deletion spec to decide whether the entry
may proceed as auto or must be treated as confirm (already gated
upstream by the clean skill). delete SHALL perform a git rm (recursive
for a directory-rule aggregate entry) staged into the run's single hygiene
commit. extract-then-delete SHALL first complete its generative extraction
step (repo-durable via the existing live-read Sonnet distillation path
writing into an ADR/CLAUDE.md/docs target, or cross-repo via
/os-vault:write) and SHALL only perform the git rm once extraction has
succeeded; both steps SHALL land in the same single hygiene commit, or, on
extraction failure, neither SHALL be applied for that entry (skip, not a
run-level hard failure, unless the failure matches an existing hard-failure
trigger). When the extraction destination is the vault (the content
physically leaves the repo), the op SHALL additionally append a pointer
entry to the deleted file's per-directory extracted.md index (creating the
file if absent), in the same atomic sequence — distill → /os-vault:write →
append the pointer line → git rm — all staged into the same single hygiene
commit, so there is no window where the doc is gone but undiscoverable. The
pointer entry SHALL name the vault note, state why a future reader would
follow it, and record the source filename and date. Repo-durable extraction
targets (ADR, CLAUDE.md, docs) SHALL NOT produce an index entry — they are
already discoverable in-repo.
Scenario: delete performs a true git rm at apply time
- WHEN the applier applies a
deleteentry - THEN it re-verifies tracked/clean status via
git ls-filesand a dirty check, then performs agit rmstaged into the single hygiene commit
Scenario: extract-then-delete only deletes after extraction succeeds
- WHEN the applier applies an
extract-then-deleteentry - THEN it completes the extraction write (ADR/CLAUDE.md/docs or vault) first, and performs the
git rmonly after that write succeeds
Scenario: A failed extraction skips the delete for that entry
- WHEN the extraction step of an
extract-then-deleteentry fails - THEN the delete is not applied for that entry, the entry is reported as skipped, and the run is not treated as a hard failure unless the failure matches an existing hard-failure trigger (applier exit 2, write error)
Scenario: Directory-rule aggregate delete removes the whole directory
- WHEN the applier applies a
deleteentry whose path is a directory-rule aggregate entry - THEN it performs a recursive
git rmremoving the entire matched directory in one operation staged into the single hygiene commit
Scenario: A vault extraction leaves an extracted.md pointer in the same commit
- WHEN an
extract-then-deleteentry extracts to the vault via/os-vault:write - THEN a pointer entry naming the vault note, the reason to follow it, the source filename, and the date is appended to the deleted file's directory
extracted.md(created if absent), and the append, the deletion, and the index file are all staged into the same single hygiene commit
Scenario: A repo-durable extraction leaves no index entry
- WHEN an
extract-then-deleteentry extracts into an ADR, CLAUDE.md, or docs target inside the repo - THEN no
extracted.mdentry is written — the residue is already discoverable in-repo
Scenario: A failed pointer append skips the delete
- WHEN the vault write succeeds but appending the
extracted.mdpointer fails - THEN the
git rmis not applied for that entry and it is reported as skipped, preserving the invariant that a doc is never gone but undiscoverable
Requirement: Clean Skill Orchestration
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
generative (always confirm) subsets. It SHALL gate confirm entries before any
mutation. It SHALL run the git preflight (clean check / WIP checkpoint). It SHALL
apply deterministic approved entries via patch_applier.py. It SHALL dispatch
generative approved entries to a Sonnet subagent via workflows/distill.md
(LOOP-GUARD subagent pointer). It SHALL stage precisely and commit once via
git-context commit-apply --message-stdin. It SHALL stamp last_clean. It SHALL
surface applied/skipped (with re-analysis notes) / confirmed counts and the commit
SHA.
Scenario: No report — prompt to check first
- 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
- WHEN the loaded report fails re-validation
- THEN the skill stops and reports the validation error without modifying anything
Scenario: Zero in-scope entries — no-op report
- WHEN all entries are filtered out by scope/category
- THEN the skill reports zero in-scope entries and exits without modifying the tree or creating a commit
Scenario: Full pipeline succeeds
- WHEN the report contains valid entries and the user approves all confirm entries
- THEN the skill applies deterministic entries via the applier, dispatches generative entries to Sonnet, stages precisely, commits once, stamps last_clean, and surfaces the commit SHA
Requirement: Generative Distillation via Live-Read Sonnet Subagent
For approved generative entries, the clean skill SHALL confirm the file still exists
(a preceding move-to-archive on the same file would have removed it). It SHALL read
the live file contents at dispatch time (not from the report cache) to guarantee
freshness, because generative entries carry no expected_sha256. It SHALL dispatch
to a Sonnet subagent pointing at workflows/distill.md (LOOP-GUARD — the subagent
reads that workflow, not SKILL.md, to avoid recursion). The subagent SHALL return
new prose (or new-primary + archived-section for split). The skill SHALL write and
stage the result. A file with both a generative entry and a deterministic entry SHALL
be treated as incompatible-ops-on-file and skipped.
Scenario: File missing at dispatch time — skip
- WHEN a generative entry targets a file that no longer exists (e.g., moved by a prior step)
- THEN the skill skips the generative entry and reports it as skipped
Scenario: Live read ensures freshness
- WHEN the skill dispatches a generative entry to Sonnet
- THEN it reads the file's current bytes immediately before dispatch, not from the cached report span
Scenario: Generative + deterministic on same file — incompatible
- 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
/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 its own sweep skill
(skills/sweep/SKILL.md), not by having the clean skill invoke check internally.
Scenario: Sweep routes through its own skill, not by nesting clean inside check
- WHEN the user runs /os-doc-hygiene:sweep
- THEN the
sweepskill invokes thecheckskill then thecleanskill sequentially, passing shared flags
Scenario: Sweep produces at most one cleanup commit
- 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