198 lines
11 KiB
Markdown
198 lines
11 KiB
Markdown
|
|
# 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).
|