# 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).
