cc-os/openspec/changes/archive/2026-07-03-add-os-adr-plugin/design.md

198 lines
11 KiB
Markdown
Raw Normal View History

# Design: add-os-adr-plugin
## Context
The requirements are locked in `docs/adr-system/04-plugin-requirements.md` and phased in
`docs/adr-system/05-plugin-prd.md` — this design decides *how* to build them, not *what* they
are. cc-os already has three global plugins establishing the conventions this one must follow:
- `os-vault` — Python hooks (thin entry points + shared `config.py`/`hook_io.py` modules), hooks
wired by absolute path in `settings.json`, verb-first skills, no `commands/` dir.
- `os-doc-hygiene` — plugin-relative `hooks/hooks.json` using `${CLAUDE_PLUGIN_ROOT}`,
deterministic zero-token SessionStart reminder with per-project gitignored state dir
(`.dochygiene/`) and snooze, deterministic-scanner + generative-only-where-needed split,
invariants.md + golden-example fixtures.
- `os-orchestration` — minimal SessionStart context injection.
Constraints: single template everywhere (no variants), `docs/adr/` location, mechanical-first
with LLM reserved for judgment, non-destructive migration, OO per Sandi Metz, eval deferred.
## Goals / Non-Goals
**Goals:**
- A working `os-adr` plugin at `cc-os/plugins/os-adr/`, installed via the `local-plugins`
marketplace, covering PRD Phases 14 (authoring, session hook, migration, retrieval).
- Every deterministic behavior testable without a model; every LLM-assisted behavior isolated
behind a skill so the mechanical core stays pure.
- Eval scenario *sketches* written down so the plugin surface is eval-ready.
**Non-Goals:**
- The unprompted-behavior evaluation itself (separate later stage, `autoresearch` loop).
- Retrofitting cc-os's own ADR file, bulk rollout beyond the pilot projects.
- Per-project template variants, severity/enforcement fields, multi-editor governance.
## Decisions
### D1 — Plugin layout and packaging
`cc-os/plugins/os-adr/` with:
```
os-adr/
.claude-plugin/plugin.json # name: os-adr
hooks/hooks.json # SessionStart wiring via ${CLAUDE_PLUGIN_ROOT}
hooks/session_start.py # thin Python entry point (deterministic check)
lib/ # Ruby OO core (adr/ classes)
bin/ # Ruby CLI entry points the skills invoke
templates/adr.md # the customized-Nygard template
skills/create/SKILL.md
skills/init/SKILL.md
skills/migrate/SKILL.md
skills/find/SKILL.md
tests/ # unit tests + golden-example fixtures
invariants.md
```
Install follows the CLAUDE.md three-place procedure: symlink
`~/.claude/plugins/os-adr → cc-os/plugins/os-adr/`, add `"os-adr"`/`"./os-adr"` to the
`local-plugins` marketplace manifest, enable `os-adr@local-plugins` in `settings.json`, then
`claude plugin marketplace update` + `claude plugin install`, and verify with
`claude plugin details` (skills registered is the check that catches manifest mistakes).
**Why:** matches all three existing plugins; the manifest step is the known failure mode
(ADR-018) so it's explicit in tasks.
### D2 — Hook wiring: plugin `hooks.json`, not `settings.json` absolute paths
Use the `os-doc-hygiene` pattern (`hooks/hooks.json` with `${CLAUDE_PLUGIN_ROOT}`, matcher
`startup|resume`, short timeout). **Why over the os-vault absolute-path pattern:** it keeps the
plugin self-contained, survives repo moves, and hook registration then fails *with* plugin
registration rather than silently diverging from it (the masking problem ADR-018 documented).
### D3 — Language split: Ruby OO core, Python only for the hook entry point
- `lib/` is Ruby, Sandi Metz style: small single-responsibility classes
(`Adr::Template`, `Adr::Record`, `Adr::Index`, `Adr::Repository`, `Adr::Detector` (migration
shape detection), `Adr::Migrator`, `Adr::Finder` (retrieval pipeline)), dependency injection
(paths and IO handed in, no globals), tell-don't-ask.
- `bin/` exposes thin Ruby CLIs (`adr-new`, `adr-init`, `adr-detect`, `adr-migrate`,
`adr-find`) that skills call via Bash. (No standalone `adr-index` — index regeneration is
embedded in every writing CLI, per D4.)
- The SessionStart hook is a thin Python script that does the existence check inline (stat two
paths, read one suppression file, emit JSON) — it does **not** shell out to Ruby.
**Why:** the user's standing Ruby preference for the core; Python for the hook matches the
existing hook pattern and keeps session-start latency free of Ruby interpreter spin-up. The hook
logic is ~20 lines and purely mechanical, so duplicating "does `docs/adr/` exist" between Python
and Ruby is acceptable coupling (alternative — Python wrapping the Ruby core — was rejected for
latency and dependency-surface reasons at session start).
### D4 — Template and ID/index mechanics
- Template (`templates/adr.md`): frontmatter `id`, `date`, `status`
(Proposed/Accepted/Superseded/Deprecated), `supersedes`, `superseded-by`, `affected-paths`,
`affected-components`, `migration_confidence` (migration-written files only); body sections
`## Context`, `## Decision`, `## Consequences`, `## Alternatives rejected`.
- Files: `docs/adr/NNNN-kebab-title.md`, 4-digit zero-padded sequential ID; next ID = max
existing + 1, derived by scanning the directory (never trusting the index).
- Index: `docs/adr/README.md`, a generated table (ID, title, status, date) between markers;
regenerated in full from the directory on every write by `Adr::Index` — never hand-edited,
never appended-to incrementally. Setting `superseded-by` on an old ADR when a new one declares
`supersedes` is also mechanical (`adr-new` does both edits).
**Why full regeneration over append:** idempotent, self-healing after manual file edits, trivial
to test against golden fixtures; append-only indexes drift.
### D5 — SessionStart behavior and suppression
- `docs/adr/` + index present → inject one short additionalContext line: the system exists, how
to write (`/os-adr:create`) and query (`/os-adr:find`) it. Near-zero tokens, no LLM.
- Absent → one-line suggestion to run `/os-adr:init` or `/os-adr:migrate`.
- Suppression: per-project gitignored state dir `.os-adr/` containing `suppress` (permanent
opt-out for deliberately ADR-less projects) — same shape as `.dochygiene/`. `/os-adr:init`
clears it; hook exits silently when present. A once-per-day snooze stamp (`last_reminded`)
additionally rate-limits the absent-case nag, mirroring os-doc-hygiene.
### D6 — Migration pipeline (mechanical/LLM boundary)
`adr-detect` (mechanical) classifies existing content into the surveyed shapes — numbered
per-file dirs, dated single files, monolithic multi-decision files, prose-embedded — plus
`unrecognized` as the fallback (reported, never auto-converted). `adr-migrate` then, per source
decision:
- **Heuristic fill (mechanical):** fields structurally unambiguous in the source — Status/Date
from frontmatter or clear headings, title, ID assignment, source-file provenance link.
- **LLM fill (skill-mediated, flagged):** interpretive fields only — Consequences,
Alternatives-rejected, splitting a monolithic file into per-decision units. The Ruby core
emits a work manifest of exactly which fields need LLM fill; the `migrate` skill has the model
fill them and hands results back to the core for writing. Never invent Decision or Context
from nothing — if absent in the source, the field carries an explicit `_not stated in
source_` marker and the file is flagged.
- **Flagging (one mechanism):** `migration_confidence: low|medium|high` frontmatter per file +
one `docs/adr/migration-report.md` summarizing per-file confidence, source mapping, and the
overall flag rate. No scattered inline comments.
- **Non-destructive:** new files only; source files untouched. Old-system deletion is a
separate, explicit user-approved step the skill offers only after the report is reviewed.
- **Pilot gate:** run on 23 surveyed projects (viking-warrior-training-log, delta-refinery,
llf-schema); if the low-confidence flag rate exceeds the threshold set at pilot time
(recorded in the report), tighten heuristics before wider rollout. Build-phase gate, not a
shipped feature.
### D7 — Retrieval stack (`adr-find` / `/os-adr:find`)
Deterministic layers first, each narrowing the candidate set:
1. Path/component match: query paths (e.g. files being edited) against `affected-paths` /
`affected-components` frontmatter across `docs/adr/`.
2. Status filter: `Accepted` only by default (`--all-statuses` to override).
3. Graphify traversal: when `graphify-out/` exists, expand the *query* paths one hop via
graph-node `source_file` attributes and re-match the expanded set against `affected-paths`
ADRs themselves are never graph nodes (resolved during Phase 4; see ADR-020). Reuses
os-vault's project-graph infrastructure; degrades gracefully to layers 12 when absent.
4. AI judgment (skill-mediated) only over the narrowed set — the `find` skill presents the
candidates and the model picks/ranks; it never sweeps the full corpus.
This is a mid-task-callable surface (skill + CLI), satisfying the PRD's eval-readiness note —
retrieval is not locked to SessionStart.
### D8 — Eval-readiness sketches
A `docs/adr-system/06-eval-scenarios.md` sketch (write-trigger + retrieval scenario shapes per
the PRD) is written during this change, and the SessionStart context line explicitly names both
surfaces (`create`, `find`) so an agent can discover them unprompted. The eval itself is out of
scope.
## Risks / Trade-offs
- [Duplicated existence-check logic between Python hook and Ruby core] → it's two path checks;
covered by a shared fixture test asserting both agree on the same tree.
- [Ruby runtime required at skill time] → acceptable: this is the user's own machine and
standing preference; hook path (the always-runs surface) has no Ruby dependency.
- [Migration LLM fills may be wrong] → confidence flags + report + pilot gate before rollout;
non-destructiveness means a bad migration is discardable (delete `docs/adr/`, rerun).
- [Graphify layer depends on os-vault onboarding state] → retrieval degrades to layers 12;
layer 3 is an enhancement, not a dependency.
- [Monolithic-file splitting is the hardest detection case] → treated as LLM-boundary work with
per-unit flags; cc-os's own 19-ADR file is deliberately excluded from the pilot.
- [Index regeneration overwrites manual index edits] → by design (index is declared
never-hand-edited in its own header comment).
## Migration Plan
Build order matches PRD phases: Phase 1 (template+create+index) → Phase 2 (hook) → Phase 3
(migration) → Phase 4 (retrieval), each verifiable independently. Install/registration happens
after Phase 1 so `create` is dogfoodable early. Rollback: uninstall plugin + remove symlink and
manifest entry; projects keep their `docs/adr/` files (plain markdown, tool-independent).
## Open Questions
- Exact low-confidence flag-rate threshold for the pilot gate — set empirically at pilot time
and recorded in the migration report (deliberately not guessed now).
- ~~Whether ADR frontmatter needs a dedicated Graphify tag/edge convention for layer-3
traversal~~ — **Resolved during Phase 4 (2026-07-03):** no convention needed. Layer 3 expands
the *query* paths one hop via graph-node `source_file`s; ADRs never need to be graph nodes and
match purely on `affected-paths` frontmatter (verified against the real llf-schema project
graph; recorded in ADR-020).