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.
<2 paragraphs of project overview redacted as it's not relevant>
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 issuesRun 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.
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.
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
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
Typical: Direct CDN links or package.json
Our preference: Vendored assets in ./vendor
Reason: Works offline and no build
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
Typical: rake tasks
Our preference: thor tasks
Reason: It's more expressive and allows defining arguments
Typical: 3rd party gem
Our preference: ./lib/replicate.rb
Reason: There are no good gems and it's easier to wrap it ourselves
Typical: application.html.erb
Our preference: _base.html.erb that other layouts inherit
Reason: This allows us to consistently have the same
wrapper everywhereTypical: 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
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
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
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.
## 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"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-permanentdoes NOT need an ID to track elements across renders- Use Stimulus syntax for key events, e.g.
keydown.meta+s@window->editor#save