Skip to content

Instantly share code, notes, and snippets.

@tvararu
Last active November 6, 2025 03:10
Show Gist options
  • Select an option

  • Save tvararu/e25d263021c643462473d0ef0a72a111 to your computer and use it in GitHub Desktop.

Select an option

Save tvararu/e25d263021c643462473d0ef0a72a111 to your computer and use it in GitHub Desktop.
A hand-written AGENTS.md from a recent Rails project

Agents

Last updated on: 2025-11-02

This file provides guidance to agents when working in this repository, whether they be AI or human.

It is hand-written, with every word carefully considered by a human. Follow it very closely.

Overview

<2 paragraphs of project overview redacted as it's not relevant>

Development

Run everything via mise tasks defined in mise.toml. The most important ones:

$ mise tasks
Name            Description
setup           bin/setup --skip-server equivalent, bundle and restarts server
ci              Use this after finishing a piece of work
test:all        Run all tests separately from mise ci
rubocop:fix     Auto-fix Rubocop issues
rufo:fix        Auto-fix Rufo issues

Run rails generate or rails db tasks only when absolutely necessary. The user will always inspect these carefully.

Never run bin/bundle exec commands unless instructed. Use rails or bundle directly.

Never try to run rails console. Never start the server yourself, the user is already running it.

If you need to try snippets of Ruby code, run one-liners via rails runner, or save longer ones to ./tmp/ and run them.

Tech stack

This is a standard Ruby 3 and Rails 8 application.

The tech stack is as close to vanilla Rails as possible:

  • Puma
  • PostgreSQL 18
  • Propshaft, Importmap
  • Stimulus, Turbo, Hotwire
  • Solid Cache, Solid Queue, Solid Cable
  • Active Storage
  • Minitest, fixtures, capybara, selenium, few system tests
  • rails credentials
  • Kamal
  • GitHub actions
  • Rubocop Omakase
  • View transitions

The objective is to stay as close to vanilla Rails as possible and introduce only necessary deviations. These are detailed below.

mise

Typical: .ruby-version, rvm, rbenv, bin scripts

Instead: mise.toml for everything

Reason: It replaces rvm, make, dotenv, and other tools, and the muscle memory is consistent across projects. .ruby-version is kept in sync for CI

pgvector

Typical: Dedicated vector database

Our preference: pgvector via ./docker-compose.yml or Kamal accessory

Reason: It's very lightweight and easy to use, docker makes it easy

Vendored frontend assets

Typical: Direct CDN links or package.json

Our preference: Vendored assets in ./vendor

Reason: Works offline and no build

Tailwind and DaisyUI

Typical: package.json with Tailwind and plugins

Our preference: Tailwind v4 with no package.json, pure Ruby gem implementation. DaisyUI is bundled in the assets folder. Puma starts Tailwind plugin.

We use Heroicons but we simply bundle the SVG files in ./vendor and include them using a short ./lib/heroicon.rb class

Reason: No build is superior

Thor tasks

Typical: rake tasks

Our preference: thor tasks

Reason: It's more expressive and allows defining arguments

Replicate

Typical: 3rd party gem

Our preference: ./lib/replicate.rb

Reason: There are no good gems and it's easier to wrap it ourselves

Layout

Typical: application.html.erb

Our preference: _base.html.erb that other layouts inherit

Reason: This allows us to consistently have the same

wrapper everywhere

Active Storage attachment linking

Typical: Automatically generated redirect URLs

Our preference: Direct url to ./storage/cdn via ./public/cdn symlink

Reason: The Rails redirect URLs are very slow and brittle

UI helpers and form builders

Typical: Duplicate Tailwind classes all over the codebase, or components gem

Our preference: ./app/helpers/ui_helper.rb and ./app/helpers/ui_form_helper.rb

Reason: We don't use link_to but ui.link_to to avoid scattering the same Daisy or Tailwind classes everywhere

Player.js

Typical: Full React TypeScript frontend with build step

Our preference: Bundled preact with htm and JSDoc

Reason: No build is superior, JSDoc gives LSPs the same benefits as TS

Styleguide

Write self-describing, terse, beautiful, idiomatic Ruby code. It should look as good as the Rails source code. When in doubt, ask yourself "What would DHH do?"

Write surgical tests, don't be exhaustive. Prefer tests that increase coverage. Remove tests that don't.

Prefer controllers with the standard verbs: index, show, new, create, edit, update, destroy. Create new ones instead of tacking on non-standard methods. Use links for GET, buttons for POST PUT PATCH DELETE.

Prefer context7 and web search to look up documentation. Read source code of dependencies often. Replace github.com links with uithub.com to read a LLM-friendly summary of a repository.

Prefer no-JS solutions with progressive enhancement. Use Turbo and play to Rails' strengths. Reach for Stimulus and custom JS only as a last resort.

Don't write comments. Don't remove existing comments, like model annotations.

Don't use service objects. Don't create new folders in ./app, prefer ./app/lib.

Don't disable strict loading, add includes or preloads as necessary.

Code examples

## Rule: Prefer symbol-to-proc shorthand (ampersand colon)
# Bad
users.map { it.process }

# Good
users.map(&:process)

## Rule: Prefer it and _1
# Bad
users.map { |user| analyze(user) }

# Good
users.map { analyze it } # Single parameter, always use it
hash.map { [_1, _2.upcase] } # Multiple parameters

## Rule: Use Struct, never OpenStruct
# Bad
person = OpenStruct.new(name: "Alice", age: 30)

# Good
person = Struct.new(:name, :age).new("Alice", 30)

## Rule: Wrap code at 80 chars
# Bad
SomeService.new(user).perform(options[:action], options[:params], options[:callback])

# Good
SomeService.new(user).perform(
  options[:action], options[:params], options[:callback]
)

## Rule: Use %i[] for symbol arrays
# Bad
enum :status, [:pending, :processing, :completed, :failed]

# Good
enum :status, %i[pending processing completed failed]

## Rule: Use hash shorthand
# Bad
User.create(name: name, email: email)

# Good
User.create(name:, email:)

## Rule: Pass globalid to jobs, never id
# Bad
ProcessUserJob.perform_later(user.id)

# Good
ProcessUserJob.perform_later(user)

## Rule: Don't use low-opacity text for empty states, it harms accessibility
# Bad
<p class="text-base-content/50">No content yet</p>

# Good
<p>No content yet</p>

## Rule: Use DaisyUI semantic colors
# Bad
<div class="bg-red-500">Error</div>

# Good
<div class="bg-error-content">Error</div>

## Rule: Use ui helpers for standard components
# Bad
<%= link_to "Show", show_path, class: "link link-primary" %>
<%= link_to "Edit", edit_path, class: "btn btn-primary" %>
<%= button_to "Delete", destroy_path, class: "btn btn-error" %>

# Good
<%= ui.link_to "Show", show_path %>
<%= ui.button_link_to "Edit", edit_path, variant: :link %>
<%= ui.button_to "Delete", destroy_path, variant: :error %>

## Rule: Use preload in jobs
# Bad
class ProcessOrderJob < ApplicationJob
  def perform(order)
    order = Order.preload(:items).find(order.id)
    order.items.each(&:process)
  end
end

# Good
class ProcessOrderJob < ApplicationJob
  preload %i[items]

  def perform(order)
    order.items.each(&:process)
  end
end

## Rule: Use class_names helper for conditional classes
# Bad
class: "tab #{is_active ? 'tab-active' : ''}"

# Good
class: class_names("tab", "tab-active": is_active)

## Rule: Use endless methods for one-liners
# Bad
def perform(generation)
  generation.process!
end

# Good
def perform(generation) = generation.process!

## Rule: Broadcast refreshes from models, not controllers
# Bad (in controller)
@agent.update(mode: :plan)
broadcast_refresh(@agent)

# Good (in model)
class Agent < ApplicationRecord
  broadcasts_refreshes
end

## Rule: Use guard clauses and early returns over nested conditionals
# Bad
def create
  if valid?
    if authorized?
      # do work
    end
  end
end

# Good
def create
  return unless valid?
  return unless authorized?
  # do work
end

Memories

"Regulations are written in blood." - Aviation saying

The following memories were added only after countless mistakes were made because they were not followed. Treat them with the utmost consideration.

  • data-turbo-permanent does NOT need an ID to track elements across renders
  • Use Stimulus syntax for key events, e.g. keydown.meta+s@window->editor#save
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment