From ruby-programming
Use when writing, reviewing, or refactoring Ruby code, Rails controllers, ActiveRecord models, RSpec specs, service objects, or background jobs. Also use when reviewing PRs that contain Ruby changes. Triggers on Ruby, Rails, RSpec, ActiveRecord, Sorbet, RuboCop, Sidekiq, Puma.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ruby-programming:ruby-programmingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
OOP design, type safety, refactoring discipline, and code quality — patterns Claude wouldn't apply by default.
OOP design, type safety, refactoring discipline, and code quality — patterns Claude wouldn't apply by default.
T.untyped. Use T.nilable to handle nilability explicitly.For each unit of work, cycle through these stages. The code starts simple and ends well-factored — the iteration is the point.
Before writing anything: What messages need to be sent? What are the dependencies? Object or data structure? Sketch the public interface in your head, not the implementation.
Design analysis — read references/design-shapes.md and references/design-vocabulary.md, then answer:
Write a single test for the simplest behavior. Use the message testing matrix to decide what to assert. Run it (or confirm it would fail). Do not write implementation yet.
Write only the code needed to make that one test pass. This means:
The test passing is the gate. If the test passes, Green is done.
Now improve the code. Two passes — mechanical first, then design.
Mechanical pass — apply flocking rules, one change at a time, tests green after each:
Each improvement is a separate change. If you can't describe it in five words, it's more than one change.
Design pass — after mechanical cleanup, step back and read references/design-shapes.md:
Go back to step 2 with the next behavior. Repeat the Red → Green → Refactor cycle until all behaviors are implemented. The code grows incrementally — each cycle adds one concept.
Before submitting to the verifier, do a quick mechanical pass over your code. These are the most common first-pass mistakes — binary checks, not judgment calls:
T.must has an inline WHY comment proving nil is impossibleT.untyped for known structures — use T.type_alias, T::Struct, or explicit types. Only at real boundaries (untyped gems, deserialized data)k/v/i (check block params especially)it block has one expect (split multi-assertion tests)allow + have_received), not tested via side effectsmap/select/flat_map), not imperative << loopsFix anything you catch before spawning the verifier. This reduces verify/fix round-trips from ~4 to ~1-2.
When the pre-flight sweep is clean, spawn the ruby-verifier agent with only the changed file paths and the quality checklist path (~/.claude/skills/ruby-programming/references/quality-checklist.md). No implementation context, no justifications — fresh eyes. The agent returns PASS/FAIL per checklist item. Fix all FAILs, return to the appropriate stage (Design for structural issues, Refactor for smells), and spawn the agent again. Always use the sub-agent — never self-verify.
Accumulate context across passes. On each subsequent verify pass, include: (1) which prior FAILs were fixed, (2) which were overridden and why. This prevents the verifier from re-litigating resolved decisions and lets the loop converge. Without this context, each fresh-eyes pass finds different issues and the loop oscillates instead of converging.
Overriding a FAIL: You may disagree with a finding when broader context justifies it — but you must cite which skill principle applies (e.g., "codebase consistency, Principle #1") and state the reason inline. Silent dismissals are not overrides. If you can't name the principle, fix the code.
The loop exits when the verifier returns zero new FAILs. Then ask: is this the best code we can write? Can we simplify or clarify anything further? If yes, repeat the loop. If no — congrats, ship it.
k (key), v (value), i (index). Everything else gets full descriptive names.fetch/retrieve/get for the same operation. Don't pun — add for arithmetic vs add for collection insert are two different methods.ProductInfo and ProductData mean the same thing, you have a naming failure.dry_run: switching between commit and rollback, async: switching between sync and enqueue), split into two well-named methods. Legitimate exceptions: Rails-idiomatic modifiers (validate: false), query filters (include_inactive:), and guard bypasses (force:) where the core algorithm is unchanged.if filing.active_california_filing? not if filing.state_code == 'CA' && filing.period_end > Date.today.attr_reader. Never raw @variables in method bodies.map/select/reduce/filter_map/transform_values/each_with_object over imperative loops with mutation. Prefer immutable data: freeze constants, use const in T::Struct.
filings.select(&:active?).map(&:total_tax)result = []; filings.each { |f| result << f.total_tax if f.active? }; resultdefined?(@val) ? @val : (@val = expr) — ||= fails for nil/false.config = @config; items.each { |item| process(item, config) }.find_each not each when iterating over ActiveRecord collections. each loads all records into memory.typed: strict for new files. This means T.sig on every method — Sorbet enforces it. Match the existing typing level when modifying existing files.T::Struct for data (input parameters, value objects, config). Use classes with extend T::Sig for behavior (service objects, domain logic).T::Interface. Same principle (behavior over class), statically verified.T.nilable for nullable values. Handle nilability explicitly — don't return nil and hope callers check. Avoid nil where possible.T.untyped is a code smell — use it only at real boundaries. Acceptable: inputs from untyped gems, deserialized JSON, ActiveRecord attributes on typed: false models. Not acceptable: known internal structures where you could use T.type_alias, T::Struct, or explicit tuple types like [String, Date, Date]. If you know the shape, type it. Document WHY with an inline comment every time you use T.untyped.T.unsafe sparingly. Document WHY with an inline comment every time.T.must is a code smell. It's a "trust me bro" contract that suppresses nilability warnings without handling the nil case. Too often developers add T.must because a value shouldn't be nil — but it can be, and T.must just converts a type error into a runtime crash. Before using T.must, prove the value truly cannot be nil. If it CAN be nil, handle it explicitly (raise with context, return early, use a default). Only use T.must when Sorbet's type narrowing has a genuine blind spot and you can articulate why nil is impossible.T::AbstractUtils — they serve as enforced contracts.NullEmployee that returns 0 for pay instead of forcing every caller to nil-guard.rescue Faraday::Error — they rescue YourApp::ExternalServiceError.Exception — only StandardError or specific subclasses.before_action :except (forgetting = blocks) not :only (forgetting = allows).| Message Type | Test Strategy |
|---|---|
| Incoming query | Assert the return value (state) |
| Incoming command | Assert the side effect |
| Outgoing query | Do NOT test — receiver's job |
| Outgoing command | Assert message was sent (mock) |
| Private method | NEVER test directly |
let/before), action (subject), assertion (expect). When these blur, the test is hard to read.context per scenario, not one it block with five expectations.instance_double(AgentFilingTransmission) not double('transmission').typed: strict and non-nilable, don't write expect(x).to be_present — Sorbet guarantees it at compile time. Don't guard against nil on non-nilable types.Flag any smell from the Smells table in the quality checklist (~/.claude/skills/ruby-programming/references/quality-checklist.md). The checklist is the canonical list — don't maintain a separate copy here.
When reviewing a PR or diff, run the design pass before the syntax pass. Read references/design-shapes.md and references/design-vocabulary.md first.
Design issues outrank syntax issues. A well-named method in a wrong architecture is still wrong.
Design principles drawn from:
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub jtgrenz/ruby-programming --plugin ruby-programming