diff --git a/plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/.openspec.yaml b/plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/.openspec.yaml similarity index 100% rename from plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/.openspec.yaml rename to plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/.openspec.yaml diff --git a/plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/design.md b/plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/design.md similarity index 100% rename from plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/design.md rename to plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/design.md diff --git a/plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/proposal.md b/plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/proposal.md similarity index 100% rename from plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/proposal.md rename to plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/proposal.md diff --git a/plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/specs/calibrate/spec.md b/plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/specs/calibrate/spec.md similarity index 100% rename from plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/specs/calibrate/spec.md rename to plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/specs/calibrate/spec.md diff --git a/plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/specs/doc-clean/spec.md b/plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/specs/doc-clean/spec.md similarity index 100% rename from plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/specs/doc-clean/spec.md rename to plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/specs/doc-clean/spec.md diff --git a/plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/specs/lifecycle-rulebook/spec.md b/plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/specs/lifecycle-rulebook/spec.md similarity index 100% rename from plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/specs/lifecycle-rulebook/spec.md rename to plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/specs/lifecycle-rulebook/spec.md diff --git a/plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/tasks.md b/plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/tasks.md similarity index 100% rename from plugins/os-doc-hygiene/openspec/changes/calibrate-assessment-inventory/tasks.md rename to plugins/os-doc-hygiene/openspec/changes/archive/2026-07-15-calibrate-assessment-inventory/tasks.md diff --git a/plugins/os-doc-hygiene/openspec/specs/calibrate/spec.md b/plugins/os-doc-hygiene/openspec/specs/calibrate/spec.md index b6d553c..6d748ba 100644 --- a/plugins/os-doc-hygiene/openspec/specs/calibrate/spec.md +++ b/plugins/os-doc-hygiene/openspec/specs/calibrate/spec.md @@ -106,7 +106,13 @@ reviewed the rule report. Global-rulebook writes (writing into `plugins/os-doc-hygiene/rulebook.json`) SHALL additionally require explicit human gating, distinct from project-rule confirmation, since it is a cross-repo write into cc-os. Rule removals SHALL be HITL-only in all cases, -with recorded reasoning, regardless of scope. +with recorded reasoning, regardless of scope. Every settled verdict SHALL +persist: judge `keep` verdicts become ordinary `lifetime: keep` rules in +`rules` (including exact-path singletons, per the keep-tier relaxation); +human declines at the rule report persist as `rejected` entries with +`rejected_by: "human"`; open `consult` verdicts persist to +`nominations.consults`, deduped by glob at write time. All persistence SHALL +go through the canonical rules-file writer. #### Scenario: Project rule persists on judge confirmation plus report review @@ -123,6 +129,16 @@ with recorded reasoning, regardless of scope. - **WHEN** any rule (project or global) is proposed for removal - **THEN** the removal happens only via explicit human instruction, with the reasoning recorded, never as an automatic side effect of a calibration pass +#### Scenario: A keep verdict persists as a plain keep rule + +- **WHEN** the judge's settled verdict for a cluster is that the artifacts must be retained +- **THEN** an ordinary `lifetime: keep` rule is persisted to `rules` — removing the cluster from future calibrate pools and protecting future matches — even when the glob is an exact-path singleton + +#### Scenario: A human decline persists as a rejection + +- **WHEN** the human declines a judge-confirmed rule at the rule report +- **THEN** a `rejected` entry with `rejected_by: "human"` is written, so a later haiku round cannot re-nominate the identical glob+lifetime without the judge knowing + ### Requirement: Retest Loop with Stop Conditions and Hard Cap `:calibrate` SHALL re-run its clustering pass against the shrunk unmatched @@ -166,7 +182,15 @@ recurring by convention (e.g. `PRD.md`, `HANDOFF-*.md`, or bare timestamp unique to one instance is not acceptable. A rule that currently matches only one file is acceptable; a rule that can, by construction, only ever match one file is a failed generalization and SHALL -be flagged loudly rather than silently persisted. +be flagged loudly rather than silently persisted. **Keep-tier relaxation:** +exact-path/instance globs ARE acceptable for `lifetime: keep` entries only — +this test exists to prevent bad deletion rules, and a singleton keep merely +protects; instance globs remain forbidden for `temporary` and +`delete-once-served`. When the near-miss boundary check reveals sibling +artifacts a glob misses, the fix SHALL be to enumerate the conventional +prefixes as separate rule entries, never to widen the glob to the containing +directory; container-claiming globs are justified ONLY when the directory is +wholly machine-owned (e.g. `plugins/*/.pytest_cache/`). #### Scenario: A convention-recurring name is acceptable @@ -183,6 +207,16 @@ be flagged loudly rather than silently persisted. - **WHEN** a proposed rule currently matches exactly one file - **THEN** it is acceptable if the glob's structure could match future similarly-named files; it is flagged as a failed generalization if the glob's structure can never match any file but the one it names today +#### Scenario: A singleton keep passes under the keep-tier relaxation + +- **WHEN** a proposed rule is `docs/research/clutter-pattern-inventory.md -> keep` +- **THEN** it passes despite being an exact-path instance glob, because the keep tier only protects; the same glob with lifetime `temporary` or `delete-once-served` fails + +#### Scenario: Missed siblings are enumerated, never widened to the container + +- **WHEN** the boundary check shows `autoresearch/classic-*/` misses sibling runs under `autoresearch/improve-*/` +- **THEN** the fix is a second rule entry `autoresearch/improve-*/` (same cluster), never a widening to `autoresearch/*/` or `autoresearch/**`, so keep-worthy content can still live in the container without a counter-rule + ### Requirement: Rule-Quality Tie-Breaker — Prefer the Narrower Glob When choosing between candidate globs of differing breadth for the same @@ -244,3 +278,65 @@ the finding direction). - **WHEN** a calibration pass persists zero rules - **THEN** it fails the recall floor and therefore cannot pass + +### Requirement: Deterministic Nomination Intake Filter + +Between cheap-model nomination and strong-model judgment, `:calibrate` SHALL +run a deterministic `NominationIntakeFilter` (in `calibrate_helpers.py`, no +model — invariant #6). A nomination whose glob+lifetime exactly equals a +`rejected` entry SHALL be dropped before the judge and logged in the run +summary. Surviving nominations SHALL be annotated with every related +rejection, where related means the two globs' match sets intersect on the +current shortlist (deterministic, computed from the scan). The annotations +plus all open consults SHALL enter the judge prompt as its "Nominations +memory" input section; related rejections are context for the judge, never a +veto. + +#### Scenario: Exact glob+lifetime repeat is dropped before the judge + +- **WHEN** haiku nominates `docs/research/** -> temporary` and `nominations.rejected` contains an entry with glob `docs/research/**` and lifetime `temporary` +- **THEN** the nomination is dropped at intake, never reaches the judge, and the drop is logged in the run summary + +#### Scenario: A variant flows through annotated, not blocked + +- **WHEN** haiku nominates `docs/research/drafts/** -> temporary` and `nominations.rejected` contains `docs/research/** -> temporary`, and the two globs' match sets intersect on the current shortlist +- **THEN** the nomination proceeds to the judge carrying the related rejection (its why and consider_instead) as context, and the judge may still confirm it + +#### Scenario: Open consults always reach the judge prompt + +- **WHEN** `nominations.consults` is non-empty at intake time +- **THEN** every open consult is included in the judge prompt's "Nominations memory" section, regardless of what haiku nominated this round + +### Requirement: Consult Persistence and Resurfacing + +Open `consult` verdicts SHALL persist to `nominations.consults` (deduped by +glob at write time) rather than dying with the run. Consults SHALL resurface +in `:calibrate` only — `:check` and `:clean` are unchanged — appearing in the +judge prompt and as an "Open consults" section of the rule report. A consult +SHALL exit in exactly one of three ways: (a) a human answer settles the +purpose — a normal rule is persisted and the consult entry deleted; (b) the +human deems it not rule-worthy — the entry is rewritten into `rejected` with +`rejected_by: "human"` and the human's why; (c) the human defers — the entry +stays and resurfaces next run. New rejections and consults SHALL appear in +the rule report but are not individually gated — they are memory, not +deletion authority. + +#### Scenario: A consult survives the run and resurfaces + +- **WHEN** a judge verdict is `consult` and the run ends without a human answer +- **THEN** the consult is written to `nominations.consults`, and the next `:calibrate` run surfaces it in both the judge prompt and the report's "Open consults" section + +#### Scenario: An answered consult becomes a rule and disappears + +- **WHEN** the human answers an open consult in a way that settles the artifact's purpose +- **THEN** a normal rule is persisted through the standard report flow and the consult entry is deleted in the same write + +#### Scenario: A declined consult becomes a human rejection + +- **WHEN** the human answers that an open consult's artifact class is not rule-worthy +- **THEN** the consult entry is rewritten into `nominations.rejected` with `rejected_by: "human"` and the stated reason + +#### Scenario: Consults never surface outside calibrate + +- **WHEN** `:check` or `:clean` runs against a project with open consults +- **THEN** their behavior is unchanged — consults neither appear in output nor affect any classification diff --git a/plugins/os-doc-hygiene/openspec/specs/doc-clean/spec.md b/plugins/os-doc-hygiene/openspec/specs/doc-clean/spec.md index 1019645..eb83e85 100644 --- a/plugins/os-doc-hygiene/openspec/specs/doc-clean/spec.md +++ b/plugins/os-doc-hygiene/openspec/specs/doc-clean/spec.md @@ -211,7 +211,16 @@ writing into an ADR/CLAUDE.md/docs target, or cross-repo via 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). +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 @@ -233,6 +242,21 @@ trigger). - **WHEN** the applier applies a `delete` entry whose path is a directory-rule aggregate entry - **THEN** it performs a recursive `git rm` removing 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-delete` entry 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-delete` entry extracts into an ADR, CLAUDE.md, or docs target inside the repo +- **THEN** no `extracted.md` entry 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.md` pointer fails +- **THEN** the `git rm` is 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` diff --git a/plugins/os-doc-hygiene/openspec/specs/lifecycle-rulebook/spec.md b/plugins/os-doc-hygiene/openspec/specs/lifecycle-rulebook/spec.md index 37505ed..e1894c3 100644 --- a/plugins/os-doc-hygiene/openspec/specs/lifecycle-rulebook/spec.md +++ b/plugins/os-doc-hygiene/openspec/specs/lifecycle-rulebook/spec.md @@ -17,9 +17,11 @@ The plugin SHALL ship a global rulebook at `plugins/os-doc-hygiene/rulebook.json`, resolved relative to plugin scripts, present in every project. A project MAY additionally provide a committed repo-root `.dochygiene-rules.json` override. Both files SHALL use the -envelope `{"schema_version": 1, "rules": [...]}`. The per-project override -SHALL NOT live under gitignored `.cc-os/` — it SHALL be a committed, -reviewable dotfile. +envelope `{"schema_version": 1, "rules": [...]}`. The project file MAY +additionally carry a top-level `nominations` key (see the Nominations Memory +requirement); the loader SHALL ignore unknown top-level keys, so the key is +additive to `schema_version` 1. The per-project override SHALL NOT live under +gitignored `.cc-os/` — it SHALL be a committed, reviewable dotfile. #### Scenario: Global rulebook is always present @@ -36,6 +38,11 @@ reviewable dotfile. - **WHEN** either the global rulebook or a project override is loaded - **THEN** it is validated against the envelope `{"schema_version": 1, "rules": [...]}` +#### Scenario: A nominations key does not disturb the v1 loader + +- **WHEN** a project `.dochygiene-rules.json` carries a top-level `nominations` key alongside `rules` +- **THEN** `rulebook.py` loads the `rules` array exactly as before, ignoring the unknown top-level key without warning or error + ### Requirement: Glob Dialect Is glob.translate Rule `glob` patterns SHALL be compiled using stdlib @@ -222,3 +229,65 @@ other signal. - **WHEN** `HANDOFF-2026-07-01.md` matches a file-rule with `lifetime: delete-once-served` and also has an existing broken-reference signal - **THEN** its shortlist entry carries both the lifecycle signal and the pre-existing broken-reference signal + +### Requirement: Nominations Memory Lives in the Project Rules File + +The project `.dochygiene-rules.json` MAY carry a top-level `nominations` key +holding exactly two lists: `consults` (open questions — entries with `glob`, +`question`, `evidence`, `cluster_key`, `asked_on`, and deliberately NO +lifetime) and `rejected` (settled "no" answers — entries with `glob`, +`lifetime`, `why`, optional `consider_instead`, `rejected_by` (`"judge"` or +`"human"`), `judged_on`). The `nominations` key SHALL never affect which +files the rulebook governs — only entries in `rules` decide that. +`rulebook.py` SHALL remain nomination-unaware; only the calibrate helpers +read the key, and the calibrate reader SHALL warn on unrecognized nomination +fields, mirroring the rules array's unknown-field discipline. Rejected +entries and exact-path singleton keep rules SHALL exit only by hand-deletion +(removals stay HITL with recorded reasoning); no automated revisit path +SHALL exist. + +#### Scenario: Nominations never filter files + +- **WHEN** the scanner or rulebook resolves the governing rule for a path that only a `nominations` entry's glob matches +- **THEN** the path is treated as unmatched/unmanaged — nominations carry no lifecycle authority + +#### Scenario: Consult entries carry no lifetime + +- **WHEN** a consult entry is written to `nominations.consults` +- **THEN** it records `glob`, `question`, `evidence`, `cluster_key`, `asked_on` and no lifetime field — presence in the list means open, with no status field + +#### Scenario: Unrecognized nomination fields warn in the calibrate reader + +- **WHEN** the calibrate helpers read a nominations entry containing an unknown field +- **THEN** a warning is emitted and the field is preserved, never silently dropped + +#### Scenario: A rejection leaves only by hand-deletion + +- **WHEN** a calibration pass runs against a rules file containing a stale rejection +- **THEN** no automated path removes or expires the entry; it is removed only by explicit human edit + +### Requirement: Canonical Writer-Enforced Ordering + +Every code path that serializes `.dochygiene-rules.json` SHALL write through +one canonical writer that emits: `rules` grouped by lifetime tier in the +order delete-once-served, temporary, keep, glob-sorted within each group; +`nominations` after `rules`; `consults` before `rejected`, each glob-sorted. +The writer SHALL be idempotent (canonicalizing an already-canonical file is a +no-op) and SHALL round-trip unknown fields with a warning rather than +dropping them. Ordering SHALL NOT be enforced by any hook; hand edits +re-canonicalize on the next write. + +#### Scenario: Writes are grouped and sorted canonically + +- **WHEN** the writer serializes a rules file containing rules of all three tiers plus nominations +- **THEN** the output orders rules delete-once-served → temporary → keep with globs sorted within each group, and nominations follows rules with consults before rejected, each list glob-sorted + +#### Scenario: Canonicalization is idempotent + +- **WHEN** the writer serializes a file it previously wrote, unchanged +- **THEN** the output is byte-identical + +#### Scenario: A hand-edited file re-canonicalizes on the next write + +- **WHEN** a human appends a rule out of tier order and a later calibrate run persists a new entry +- **THEN** the whole file is rewritten in canonical order in that write, with no hook involved in the interim