Blastoff Rails

The Repo Is
the Harness

Shipping an AI harness inside a Rails app, so the agent works like a teammate trained on your system, not a random autocomplete.

Avi Flombaum BlastOffRails.com avi.nyc

Demo

The model writes the code.
The harness defines the process.

Reliability, quality, conventions, efficiency (tokenomics), constraints, security, and compatibility don't come from trusting the models, better models, or better prompts. You get them by shipping the harness inside your app, for your whole team to use.

Deterministic non-determinism

Deterministic
Conventions
patterns, style, components
Security
auth, secrets, access
Workflows
testing, worktrees, Q/A
Artifacts
plans, docs, templates
Non-deterministic
Planning
epics, sprints, tasks
Architecture
solution, scale, tests
Iteration
TDD, debugging, refactoring
Design
UI, UX, layout

The harness is where you draw the line.

The cheaper, faster, more open the model, the less it knows your conventions, legacy, and constraints.

As models commoditize, the harness is the moat.

Five layers, in the repo

01
Instructions
CLAUDE.md / AGENTS.md — the entry contract.
02
Skills
.claude/skills — thin commands that route the work.
03
Docs
docs/conventions — runtime context, not moral support.
04
Hooks & guards
.claude/hooks — the path made executable.
05
Scripts
bin/ — known workflows the agent shouldn't re-derive.

Tell it how to enter the repo

CLAUDE.mdmd
## Critical Rules

1. **MANDATORY: ALWAYS run `/rg:conventions` before writing ANY code**
2. **MANDATORY: ALWAYS read the relevant `docs/` file before ANY feature**
3. **MANDATORY: ALWAYS use `/rg:plan` to plan work**
4. **MANDATORY: ALWAYS use `/rg:work` to begin work**
5. **MANDATORY: NEVER edit, delete, or mutate staging, production, or remote data**

Strong language is load-bearing

.claude/hooks.json · SessionStartmd
<EXTREMELY_IMPORTANT>
BEFORE YOU DO ANYTHING — read this and follow it.

1. *ALWAYS* Run /rg:conventions to load the index
2. *ALWAYS* Read the docs relevant to your task
3. *ALWAYS* Output a "Conventions loaded:" block
4. *NEVER* Write code without reading the docs
5. *NEVER* Skip re-reading because you read it earlier
</EXTREMELY_IMPORTANT>

Thin skills, reference files

.claude/skills/conventions/SKILL.mdmd
---
name: rg:conventions
description: Load project coding conventions before writing code.
---
# Project Conventions
Convention docs live in `docs/conventions/`, by domain.
Read the relevant ones with the Read tool before writing code.

| Doc | When to read |
|-----|--------------|
| docs/conventions/architecture/coding_patterns.md | naming, YAGNI |
| docs/conventions/hotwire/turbo_streams.md | streams, DOM IDs |
| docs/conventions/testing/rspec/overview.md | rules, anti-patterns |

Wrap the skills you want

.claude/skills/rg-plan/SKILL.mdmd
---
name: rg:plan
description: >-
  Plan a feature with automatic convention loading, then hand
  off to compound-engineering:ce-plan for structured planning.
---
# Plan With Convention Context
Orchestrate: understand → conventions → docs → plan.

**NEVER auto-invoke a work skill.** This produces a plan only.

Re-read every turn

.claude/skills/conventions/SKILL.mdmd
<EXTREMELY_IMPORTANT>
**ALWAYS re-read convention docs before writing code, even
if you read them earlier.** Context compression means earlier
reads may be gone from working memory.

**ALWAYS output this confirmation before writing any code:**

    Conventions loaded:
      - docs/conventions/architecture/coding_patterns.md
</EXTREMELY_IMPORTANT>

System doc

docs/ai_architecture.mdmd
---
title: "AI Architecture"
description: "Ai::Client service layer, provider abstraction, error hierarchy, and prompt system"
category: application
applies_to: [ai, llm, ruby-llm, anthropic, openai, dspy]
last_updated: 2026-03-26
---

# AI Architecture

The AI service layer provides a unified interface for LLM interactions.
All AI calls go through `Ai::Client` — never use provider gems directly.

## Quick Start

```ruby
# One-shot question (uses claude-haiku-4-5 by default)
answer = Ai::Client.ask("Summarize this text: #{text}")
```

Reasoning

hotwire/stimulus_patterns.mdmd
## Use `static values` for all controller state -- never query DOM on `connect()`

The server renders data attributes; the controller reads them
declaratively. Always declare type and default together for
non-String values.

bad / good

hotwire/stimulus_patterns.mdjavascript
// BAD -- queries DOM for initial state
connect() {
  const selected = this.element.querySelector("input[type=radio]:checked")
}

// GOOD -- server provides state via values
static values = { default: String }

connect() {
  if (this.hasDefaultValue) this.revealByValue(this.defaultValue)
}

End with a checklist

hotwire/stimulus_patterns.mdmd
## Checklist

- [ ] All state in `static values` with typed defaults -- not DOM queries
- [ ] `connect()` paired with `disconnect()` for all timers, listeners, references
- [ ] `data-controller` element wraps all `data-action` elements that reference it

Make commands deterministic

.claude/hooks/guard-bash.shbash
# Block direct rspec — must use bin/agent-rspec
if echo "$COMMAND" | grep -qE 'rspec\s' && ! echo "$COMMAND" | grep -qE 'bin/agent-rspec'; then
  echo "BLOCKED: Do not run rspec directly. Use bin/agent-rspec." >&2
  exit 2
fi

# Block starting the dev server
if echo "$COMMAND" | grep -qE '(bin/dev|rails\s+s\b|rails\s+server)'; then
  echo "BLOCKED: Do not start the dev server." >&2
  exit 2
fi

Close the off-ramps

.claude/settings.jsonjson
"deny": [
  "Skill(brainstorming)",
  "Skill(write-plan)",
  "Skill(using-git-worktrees)"
],
"allow": [
  "Bash(bin/agent-rspec:*)",
  "Bash(bin/rails:*)",
  "Bash(bin/rubocop:*)"
]

Delegate known workflows

scripts/rails-worktree.shbash
if [[ -f "${GIT_ROOT}/config/master.key" ]]; then
  ln -sfn "${GIT_ROOT}/config/master.key" \
          "${target}/config/master.key"
  ok "  ✓ Linked config/master.key"
fi
Is the convention being followed?
And is it even needed?

A convention is a hypothesis until you test it. You can't manage what you don't measure.

How the eval works

Sample app+ one minimal prompt
Two Docker sandboxes · Claude Agent SDK
Controlbare codebase
Treatment+ conventions · skills · hooks
Claude-as-judgescores each vs the pattern criteria

Same app, same prompt. The only variable is the harness.

Convention Grader scorecard: treatment 28/33, control 21/33, +21pp, with chain-helped, inconclusive, and natural-behavior buckets
Pattern drill-down for shared_modal_partial: treatment trials PASS using render shared/modal, control trials FAIL hand-building modal markup
Trial timelines comparing treatment (reads conventions skill and docs before first code write at 10:02) vs control (no convention reads, first write at 5:58)

Each bucket is an instruction

Treatment pass · control fail
Chain helped
The convention is real and the chain delivered it. Keep it.
Both pass
Natural behavior
The model does it anyway. The doc is for humans. Consider slimming.
Neither lands
Inconclusive
The pattern or doc is too weak. Tighten it or back it with a script.

It tells you where your determinism layer is working, wasted, or unproven.

The durable lessons

Thin skills, ref files
Checklists at the end
Re-read every turn
Small plans, not one
Explicit evocation
Executable guards
Delegate to scripts
Measure with evals

Thank you

Ship the
process

The model isn't the process. The repo is. As the models commoditize, your harness is the moat.

BlastOffRails.com avi.nyc im@avi.nyc