4.1 KiB
| id | date | status | supersedes | superseded-by | affected-paths | affected-components | migration_confidence | migration_source |
|---|---|---|---|---|---|---|---|---|
| 0025 | 2026-07-10 | Accepted | medium | docs/memory-system/03-architecture-decisions.md### ADR-025 — Standard Ruby structure for cc-os plugins (lib/ + single-dispatcher bin/, fail-soft, installed-gem-only dependencies) |
0025 — Standard Ruby structure for cc-os plugins (lib/ + single-dispatcher bin/, fail-soft, installed-gem-only dependencies)
Context
Two plugins ship Ruby (os-adr, os-backlog) and more will follow (os-notify is queued). implementation-status.md informally calls os-backlog's layout "the os-adr lib/+bin/ pattern", but no ADR pins it, and the two plugins already diverge: os-adr uses one bin/ script per verb (adr-new, adr-find, …) invoked as ruby ${CLAUDE_PLUGIN_ROOT}/bin/adr-new, inline abort error handling, stdlib only; os-backlog uses a single dispatcher bin/os-backlog <subcommand> invoked without a ruby prefix, a centralized fail_soft helper, and a lazily required installed gem (planka-api) guarded by rescue LoadError. Without a recorded standard, each new plugin re-decides and the surfaces drift.
Decision
New cc-os plugin Ruby follows this standard (deviate only with a recorded reason):
- Library:
lib/<domain>.rb+lib/<domain>/*.rb, onemodule <Domain>namespace, loaded viarequire_relative, stdlib-first. No gemspec/Gemfile inside a plugin — plugins are source trees, not gems. - CLI: a single dispatcher
bin/os-<domain>with subcommands (os-backlog style), executable with a#!/usr/bin/env rubyshebang; skills invoke${CLAUDE_PLUGIN_ROOT}/bin/os-<domain> <subcommand>with norubyprefix. One entry point per plugin keeps skill docs uniform and namespaces the verbs. - Errors: every user-reachable failure goes through one fail-soft helper printing exactly one line,
os-<domain>: <message>, exit 1. Never a raw stack trace from a skill-invoked path. - External gems: consumed as installed gems,
required lazily inside the method that needs them withrescue LoadError→ fail-soft. Never vendored, never source-coupled to a sibling repo, never shelled out to another gem's binary when the client classes suffice. Each required gem is named in the plugin README and implementation-status.md (installation is an environment prerequisite, so it must be discoverable). - Tests: minitest,
tests/all.rb+tests/test_helper.rb, with in-memory fakes for any network service so the suite runs on machines without the gem or credentials (os-backlog'sFakePlankaClientis the template).
- Rationale: os-backlog is the newer, deliberate iteration of the pattern and its choices earned their place: the dispatcher scales without
bin/sprawl, fail-soft is mandatory for network-touching plugins (ADR-023's hook-hygiene argument), and lazy installed-gem requires keep pure subcommands (e.g.resolve) working on machines where the gem is absent — verified in practice 2026-07-10.
Consequences
New cc-os plugin Ruby code must follow a standard structure: a lib/ namespace with require_relative loading, a single dispatcher bin/os- script for all subcommands, one fail-soft error helper per plugin, external gems required lazily with LoadError rescue and no vendoring, and minitest tests using in-memory fakes for network services. os-adr's older per-verb-script pattern is grandfathered rather than retrofitted immediately, and packaging plugins as real gems was rejected as unnecessary release ceremony.
Alternatives rejected
Per-verb bin scripts (os-adr style) — fine at 4 verbs, sprawl at 9; grandfathered in os-adr, not worth a retrofit until os-adr is next touched for other reasons. Packaging plugins as real gems — adds release/version ceremony for code that ships by symlink + cache refresh (ADR-018). Vendoring external gems — duplicates code and hides the dependency instead of documenting it.
- Cross-references: ADR-023 (plugin boundaries, hook hygiene); implementation-status.md os-adr/os-backlog component sections;
cc-os-plugin-skill-naming-convention.md(naming layer above this structural layer).