cc-os/docs/adr/0025-standard-ruby-structur...

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):

  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, required 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- 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).