PACE with Ruby (Rails)
This tutorial shows how to configure PACE for a Ruby on Rails project. FORGE will write idiomatic Ruby following Rails conventions, GATE will run RSpec or Minitest, and SENTINEL will audit for Rails-specific security risks. By the end you will have a working pace.config.yaml, a three-story sprint plan, and a clear picture of how FORGE, GATE, and SENTINEL interact with a Rails codebase.
Prerequisites
- Ruby 3.2 or later
- Rails 7.1+
- RSpec or Minitest
- An API key for your LLM provider
Project layout assumed by this tutorial
my-api/├── pace/│ └── pace.config.yaml├── app/│ ├── controllers/│ ├── models/│ └── services/├── spec/ ← or test/ for Minitest├── Gemfile└── .pace/1 — Install PACE
# From your repo rootgit clone https://github.com/pace-framework-org/pace-framework-starter pacecd pacepython -m venv .venv && source .venv/bin/activatepip install PyYAML jsonschema anthropic2 — Configure for Ruby
Edit pace/pace.config.yaml:
framework_version: "1.0"
product: name: "My API" description: > A Ruby on Rails 7.1 REST API. Service objects live in app/services/; controllers are thin and delegate immediately to services. Business logic never lives in models. Uses RSpec with FactoryBot for test fixtures and DatabaseCleaner with the transaction strategy for fast test isolation. All REST routes are resourceful. Concerns are used for shared model behaviour. github_org: "my-org"
sprint: duration_days: 14
source: dirs: - name: "app" path: "app/" language: "Ruby" description: "Rails controllers, models, and service objects" - name: "spec" path: "spec/" language: "Ruby" description: "RSpec unit and request specs"
tech: primary_language: "Ruby 3.2 / Rails 7.1" ci_system: "GitHub Actions" test_command: "bundle exec rspec --format progress" build_command: "bundle exec rubocop --no-color -f quiet"
platform: ci: local
llm: provider: anthropic model: claude-sonnet-4-6RSpec vs Minitest variants
| Setup | test_command |
|---|---|
| RSpec (default) | bundle exec rspec --format progress |
| RSpec (specific directory) | bundle exec rspec spec/services/ --format progress |
| Minitest | bundle exec rails test |
| Minitest (specific directory) | bundle exec rails test test/services/ |
| With SimpleCov | COVERAGE=true bundle exec rspec |
3 — FORGE hints for Ruby
Describe your conventions in product.description. FORGE reads this before writing any file.
Effective hints for Rails projects:
product: description: > Service objects in app/services/. Controllers call services; services call models. Use RSpec with FactoryBot for test fixtures. Never use fixtures/ files. All RSpec tests in spec/. Unit tests in spec/services/. Request specs in spec/requests/. Use DatabaseCleaner with transaction strategy for fast test isolation. Follow Rails conventions: snake_case, RESTful routes, concerns for shared model behavior. Use frozen_string_literal: true in all Ruby files.4 — Set credentials
export ANTHROPIC_API_KEY="sk-ant-..."5 — Write a Ruby sprint plan
Create plan.yaml at your repo root:
release: v1.0
stories: - id: story-1 title: "Subscription model, migration, and repository service" status: pending acceptance_criteria: - "Subscription model has user_id, plan, and status columns" - "Migration creates the subscriptions table with correct types and indexes" - "SubscriptionRepository service wraps ActiveRecord with create, find, and list_by_user methods" - "RSpec tests cover create, find by id, and list_by_user filtering" - "bundle exec rspec exits 0" out_of_scope: - "HTTP endpoints" - "Billing integration"
- id: story-2 title: "SubscriptionUpgradeService with business rules" status: pending acceptance_criteria: - "call() raises InvalidUpgradeError when downgrading to a lower plan tier" - "call() raises SubscriptionNotFoundError for an unknown subscription id" - "RSpec tests use instance_double(SubscriptionRepository) — no real database" - "bundle exec rspec exits 0"
- id: story-3 title: "POST /subscriptions and GET /subscriptions/:id endpoints" status: pending acceptance_criteria: - "POST /subscriptions returns 201 with subscription JSON on success" - "GET /subscriptions/:id returns 200 with subscription JSON or 404 if not found" - "Request specs use the Rails request spec pattern (no controller spec)" - "bundle exec rspec exits 0"6 — Run Day 1
cd pacepython pace/orchestrator.py --day 1What FORGE does with Ruby
FORGE’s tool-calling loop will:
- Read
GemfileandGemfile.lockto understand dependencies and Rails version - Read existing
app/andspec/files to learn patterns, naming conventions, and service structure - Write
.rbfiles using Rails conventions, ActiveRecord, and RSpec - Run
bundle exec rubocop(build_command) to catch style violations - Run
bundle exec rspecand read failure output to self-correct
A typical FORGE run for Day 1:
[FORGE] Reading Gemfile ...[FORGE] Reading Gemfile.lock ...[FORGE] Reading app/models/ (directory listing) ...[FORGE] Reading spec/spec_helper.rb ...[FORGE] Writing db/migrate/20240101000000_create_subscriptions.rb ...[FORGE] Writing app/models/subscription.rb ...[FORGE] Writing app/services/subscription_repository.rb ...[FORGE] Writing spec/services/subscription_repository_spec.rb ...[FORGE] Running build: bundle exec rubocop --no-color -f quiet ...[FORGE] Running tests: bundle exec rspec --format progress ...[FORGE] All tests pass. Calling complete_handoff.What SENTINEL checks for Ruby
SENTINEL applies Rails-specific security checks:
- SQL injection: string interpolation in ActiveRecord queries (e.g.
where("name = '#{params[:name]}'")) instead of parameterized form - Mass assignment:
params.permit(:all)or missing strong parameters in controllers - Secrets: credentials hardcoded in
config/database.ymlor.envfiles committed to git - Unsafe YAML:
YAML.loadwith user input (useYAML.safe_loadinstead) - Command injection: backtick execution or
system()with user-controlled input - Authentication: missing
before_action :authenticate_user!on sensitive controllers - Insecure redirect:
redirect_to params[:url]or similar without URL allowlist validation
7 — Test coverage
Use SimpleCov to enforce a coverage minimum. Add this to the top of spec/spec_helper.rb (before any other require):
require 'simplecov'SimpleCov.start 'rails' do minimum_coverage 80 add_filter '/spec/'endSimpleCov exits non-zero when coverage drops below the minimum — GATE will issue a HOLD and FORGE will write additional tests until the threshold is met.
8 — Linter and formatter
RuboCop is the standard Ruby linter. A minimal .rubocop.yml:
AllCops: TargetRubyVersion: 3.2 NewCops: enableStyle/Documentation: Enabled: falseSet the corresponding build_command:
tech: build_command: "bundle exec rubocop --no-color -f quiet"Add a lint hint to product.description so FORGE writes conforming code on the first attempt:
product: description: > Code must pass RuboCop. Max method length 20 lines. Use frozen_string_literal: true in all files.9 — Database testing patterns
SQLite in-memory (fast unit tests)
For projects where integration test fidelity matters less than speed, configure the test environment to use SQLite in memory:
test: adapter: sqlite3 database: ":memory:"DatabaseCleaner for test isolation
RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :transaction end
config.before(:each) do DatabaseCleaner.start end
config.after(:each) do DatabaseCleaner.clean endendRequire it in spec/spec_helper.rb:
require 'support/database_cleaner'Tell FORGE about the strategy in product.description:
product: description: > Tests use DatabaseCleaner with transaction strategy. Never use truncation — it is too slow. System specs that require JavaScript use the truncation strategy only.10 — Mock patterns
RSpec doubles (recommended)
describe SubscriptionUpgradeService do let(:repo) { instance_double(SubscriptionRepository) } let(:service) { described_class.new(repository: repo) }
it 'raises when subscription not found' do allow(repo).to receive(:find).and_return(nil) expect { service.call(id: 'x', plan: :pro) } .to raise_error(SubscriptionNotFoundError) endendAdd a hint to product.description so FORGE never uses unverified doubles:
product: description: > Use instance_double() for mocking, not double(). Always verify doubles match the real interface. Inject dependencies via the constructor keyword argument repository:.11 — Multi-day sprint
After Day 1 completes, SCRIBE writes a summary to .pace/context/engineering.md. When Day 2 starts, FORGE reads it before writing any new code:
[FORGE] Reading .pace/context/engineering.md ...[FORGE] Found: app/models/subscription.rb (Subscription, SubscriptionStatus, Plan)[FORGE] Found: app/services/subscription_repository.rb (SubscriptionRepository)[FORGE] Writing app/services/subscription_upgrade_service.rb ...[FORGE] Writing spec/services/subscription_upgrade_service_spec.rb ...This means story-2 acceptance criteria can safely reference classes defined in story-1 — FORGE knows they exist and requires them correctly.
12 — Advisory clearance story
SENTINEL advisories that are not auto-fixed generate a clearance story in your backlog. Common Ruby advisories:
- id: story-4 title: "Address SENTINEL advisories from Day 1" status: pending acceptance_criteria: - "Replace YAML.load calls with YAML.safe_load throughout the codebase" - "Add strong parameters (permit) to any controller action missing them" - "Replace params.permit(:all) with an explicit permit list in SubscriptionsController" - "bundle exec rspec exits 0"13 — CI build caching
The ruby/setup-ruby action handles gem installation and caching with a single option:
- uses: ruby/setup-ruby@v1 with: ruby-version: '3.2' bundler-cache: true # runs bundle install and caches gems automaticallybundler-cache: true runs bundle install and caches the installed gems keyed to your Gemfile.lock. Subsequent runs restore from cache and skip installation entirely when the lockfile has not changed.
14 — Windows notes
Ruby on Rails runs on Windows, but the smoothest experience is through WSL2. If running natively:
bundle exechandles the PATH correctly — you do not need to invokerubydirectly.- Test command paths use forward slashes:
bundle exec rspec spec/services/works on Windows. - If you encounter encoding errors, set
RUBYOPT=-Eutf-8in your shell before running PACE.
For the best experience on Windows, use WSL2 with Ubuntu and follow the standard Linux instructions.
Common issues
bundle: command not found
Ensure Bundler is installed (gem install bundler) and a Gemfile is present at the repo root. If you manage Ruby versions with rbenv or rvm, ensure the correct version is activated before running PACE.
FORGE writes files to the wrong Rails path
Describe your non-standard structure explicitly in source.dirs descriptions. For example: "Service objects in app/services/. POROs are not in app/models/." Explicit path descriptions override FORGE’s default Rails assumptions.
RuboCop fails on FORGE-generated code
Generate a RuboCop baseline for pre-existing violations (bundle exec rubocop --auto-gen-config). FORGE writes new code clean — the baseline silences only violations that existed before PACE was introduced.
DatabaseCleaner truncates too slowly
Switch to the :transaction strategy if you have not already. Reserve :truncation for system specs that open a real browser, which cannot share a transaction with the test process. See section 9 for setup.
FactoryBot factory not found
Ensure require 'factory_bot_rails' is in spec/spec_helper.rb (or rails_helper.rb) and that your factories are in spec/factories/. If you use a non-standard factory directory, add FactoryBot.definition_file_paths = ["path/to/factories"] and note the path in product.description.