CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/rails-agent-skills

Curated library of AI agent skills for Ruby on Rails development. Covers code review, architecture, security, testing (RSpec), engines, service objects, DDD patterns, and workflow automation.

98

1.38x
Quality

99%

Does it follow best practices?

Impact

98%

1.38x

Average score across 26 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdrails-code-conventions/

name:
rails-code-conventions
description:
A daily checklist for writing clean Rails code, covering design principles (DRY, YAGNI, PORO, CoC, KISS), per-path rules (models, services, workers, controllers), structured logging, and comment discipline. Defers style and formatting to the project's configured linter(s). Use when writing, reviewing, or refactoring Ruby on Rails code, or when asked about Rails best practices, clean code, or code quality. Trigger words: code review, refactor, RoR, clean code, best practices, Ruby on Rails conventions.

Rails Code Conventions

Style source of truth: Style and formatting defer to the project's configured linter(s). This skill adds non-style behavior and architecture guidance only. For Hotwire + Tailwind specifics, see rails-stack-conventions.

Linter — initial analysis

Detect → run → defer. Do not invent style rules.

  • Ruby: check for .rubocop.yml / standard gem → bundle exec rubocop or bundle exec standardrb
  • Frontend: check for eslint.config.*, .eslintrc*, biome.json, or package.json lint script → run accordingly
  • If no config is found: note this to the user — do not default to any tool.

Quick Reference

TopicRule
Style/formatProject linter(s) — detect and run as above; do not invent style rules here
PrinciplesDRY, YAGNI, PORO where it helps, CoC, KISS
CommentsExplain why, not what; use tagged notes with context
LoggingFirst arg string, second arg hash; no string interpolation; event: when useful for dashboards
Deep stacksChain rails-stack-conventions → domain skills (services, jobs, RSpec)

Design Principles

PrincipleApply as
DRYExtract when duplication carries real maintenance cost; avoid premature abstraction
YAGNIBuild for current requirements; defer generalization until a second real use case
POROUse plain Ruby objects when they clarify responsibility; do not wrap everything in a "pattern"
Convention over ConfigurationPrefer Rails defaults and file placement; document only intentional deviations
KISSSimplest design that meets acceptance criteria and tests gate

Comments

  • Comment the why, not the what (the code shows what).
  • Use tags with enough context that a future reader can act: TODO:, FIXME:, HACK:, NOTE:, OPTIMIZE:.
# BAD — restates the method name, adds zero value
# Finds the user by email
def find_by_email(email)
  User.find_by(email: email)
end

# GOOD — explains intent and tradeoff
# Uses find_by (not find_by!) so callers can handle nil explicitly;
# downstream auth layer is responsible for raising on missing user.
def find_by_email(email)
  User.find_by(email: email)
end

Structured Logging

  • First argument: static string (message key or human-readable template without interpolated values).
  • Second argument: hash with structured fields (user_id:, order_id:, etc.).
  • Do not build the primary message with string interpolation; put dynamic data in the hash.
  • Include event: (or equivalent) for error or ops dashboards when the team uses tagged events.
# BAD — interpolation loses structure; cannot filter by user_id in log aggregators
Rails.logger.info("Processing order #{order.id} for user #{user.id}")

# GOOD — static message, structured data, filterable fields
Rails.logger.info("order.processing_started", {
  event: "order.processing_started",
  order_id: order.id,
  user_id: user.id,
  amount_cents: order.total_cents
})

Apply by area (path patterns)

Rules below apply when those paths exist in the project. If a path is absent, skip that row.

AreaPath patternGuidance
ActiveRecord performanceapp/models/**/*.rbEager load in loops; prefer pluck, exists?, find_each over loading full records. Fix N+1s: run bullet → fix eager loading → re-run until clean
Background jobsapp/workers/**/*.rb, app/jobs/**/*.rbClear worker/job structure, queue selection, idempotency, structured error logging (see rails-background-jobs for Active Job / Solid Queue / Sidekiq depth)
Error handlingapp/services/**/*.rb, app/lib/**/*.rb, app/exceptions/**/*.rbDomain exceptions with prefixed codes where the team uses them; rescue_from or base handlers for API layers as conventions dictate
Logging / tracingapp/services/**/*.rb, app/workers/**/*.rb, app/jobs/**/*.rb, app/controllers/**/*.rb, app/repositories/**/*.rbStructured logging; add APM trace spans and tags (e.g. Datadog) for key operations when the stack includes them
Controllersapp/controllers/**/*_controller.rbStrong params; thin actions delegating to services; watch IDOR and PII exposure (see rails-security-review)
Repositoriesapp/repositories/**/*.rbAvoid new repository objects unless raw SQL, caching, a clear domain boundary, or external service isolation justifies it; document why in code
RSpecspec/**/*_spec.rbFactoryBot; prefer request specs over controller specs; use env: metadata (or project equivalent) for ENV changes; prefer let over let! unless the example requires eager setup; avoid before for data when let or inline factories are clearer
Serializersapp/serializers/**/*.rbIf using ActiveModel::Serializer (or similar): explicit key: mapping; avoid N+1; pass preloaded associations via options when applicable
Service objectsapp/services/**/*.rbSingle responsibility; class methods for stateless entry points, instance API when dependencies are injected; public methods first; bang (!) / predicate (?) naming as appropriate (see ruby-service-objects)
SQL securityRaw SQL anywhereNo string interpolation of user input; use sanitize_sql_array / bound parameters; whitelist dynamic ORDER BY; document why raw SQL is needed

RSpec and let_it_be (test-prof)

  • Only use let_it_be if the project already depends on the test-prof gem (check Gemfile / Gemfile.lock). Search before recommending it.
  • If test-prof is not present, follow rspec-best-practices with let as the default; use let! only when lazy evaluation would break the example (e.g. callbacks, DB constraints that must exist before the action). Explicit setup is fine when clearer. Do not require adding test-prof / let_it_be unless the user asks to introduce it.

HARD-GATE: Tests Gate Implementation

When this skill guides new behavior, the tests gate still applies:

PRD → TASKS → TEST (write, run, fail) → IMPLEMENTATION → …

No implementation code before a failing test. See rspec-best-practices and rails-agent-skills.

Common Mistakes

MistakeReality
Inventing style rules when a linter config existsThe project's configured linter is authoritative for style — do not add prose style rules
Assuming RuboCop when no config is checkedDetect first; note the absence to the user if no config is found
let_it_be in every projectUse only when test-prof is already a dependency
New app/repositories for every queryActiveRecord is the default data boundary unless there's a documented reason

Integration

SkillWhen to chain
rails-stack-conventionsStack-specific: PostgreSQL, Hotwire, Tailwind
ddd-rails-modelingWhen domain concepts and invariants need clearer Rails-first modeling choices
ruby-service-objectsImplementing or refining service objects
rails-background-jobsWorkers, queues, retries, idempotency
rspec-best-practicesSpec style, tests gate (red/green/refactor), request vs controller specs
rails-security-reviewControllers, params, IDOR, PII
rails-code-reviewFull PR pass before merge

rails-code-conventions

README.md

tile.json