---
id: "0025"
date: 2026-07-10
status: Accepted
supersedes:
superseded-by:
affected-paths: []
affected-components: []
migration_confidence: medium
migration_source: "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 `require`d 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):
  1. **Library**: `lib/<domain>.rb` + `lib/<domain>/*.rb`, one `module <Domain>` namespace, loaded via `require_relative`, stdlib-first. No gemspec/Gemfile inside a plugin — plugins are source trees, not gems.
  2. **CLI**: a single dispatcher `bin/os-<domain>` with subcommands (os-backlog style), executable with a `#!/usr/bin/env ruby` shebang; skills invoke `${CLAUDE_PLUGIN_ROOT}/bin/os-<domain> <subcommand>` with no `ruby` prefix. One entry point per plugin keeps skill docs uniform and namespaces the verbs.
  3. **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.
  4. **External gems**: consumed as *installed* gems, `require`d lazily inside the method that needs them with `rescue 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).
  5. **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's `FakePlankaClient` is 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-<domain> 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).
