# 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 1–4 (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 2–3 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 1–2 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 1–2; 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).