11 KiB
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 + sharedconfig.py/hook_io.pymodules), hooks wired by absolute path insettings.json, verb-first skills, nocommands/dir.os-doc-hygiene— plugin-relativehooks/hooks.jsonusing${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-adrplugin atcc-os/plugins/os-adr/, installed via thelocal-pluginsmarketplace, 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,
autoresearchloop). - 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 standaloneadr-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): frontmatterid,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 byAdr::Index— never hand-edited, never appended-to incrementally. Settingsuperseded-byon an old ADR when a new one declaressupersedesis also mechanical (adr-newdoes 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:initor/os-adr:migrate. - Suppression: per-project gitignored state dir
.os-adr/containingsuppress(permanent opt-out for deliberately ADR-less projects) — same shape as.dochygiene/./os-adr:initclears 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
migrateskill 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|highfrontmatter per file + onedocs/adr/migration-report.mdsummarizing 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:
- Path/component match: query paths (e.g. files being edited) against
affected-paths/affected-componentsfrontmatter acrossdocs/adr/. - Status filter:
Acceptedonly by default (--all-statusesto override). - Graphify traversal: when
graphify-out/exists, expand the query paths one hop via graph-nodesource_fileattributes and re-match the expanded set againstaffected-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. - AI judgment (skill-mediated) only over the narrowed set — the
findskill 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-nodesource_files; ADRs never need to be graph nodes and match purely onaffected-pathsfrontmatter (verified against the real llf-schema project graph; recorded in ADR-020).