
Some days, working with my coding agents on Boswell felt like whack-a-mole. Three things right, one thing wrong. Fix it. Three things right, one new thing wrong. Repeat.
It wasn’t every day. But on those days I’d see signs of confusion. An agent would loop, searching for an answer it should have had. The agents that write code and the agents that review it would go back and forth over the same point, and the gap took a long time to close.
WHACK-A-MOLE
┌──────────────┐ code ┌──────────────────┐
│ Writer agent │ ────────> │ Reviewer agents │
└──────────────┘ └──────────────────┘
^ │
│ "you broke a rule" │
└────────────────────────────┘
fix one thing, break another,
search the docs, repeat
Context sprawl
The cause was context. Some people call it context rot. Our written rules had piled up across dozens of standards docs, and a lot of them were written for an earlier time. My skills with AI tools have gotten better since then. So have the models and the coding harnesses. The rules grew faster than they got cleaned up.
You have to keep your kitchen clean.
The fundamentals aren’t judgment calls
Every project has things you always do and things you never do without a good reason. They come from the architecture you’re aiming for.
Authorization is one example. In Boswell, every controller action checks it with Pundit. Nobody needs to weigh that each time. Keeping database queries out of views is another, and so is keeping each customer’s data in its own database.
There isn’t much to think about with rules like those. So I stopped leaving them to the LLM to decide or enforce, and set the agents up to follow them. We turned those rules into custom linters and hooks: RuboCop cops for the Ruby code, Herb rules for the view templates, and Claude Code hooks for the agents themselves. Here are the parts that matter:
boswell-app/
├── AGENTS.md # the agent rules (CLAUDE.md links here): 47 lines
├── REVIEW.md # what a reviewer may block on: 42 lines
├── .githooks/
│ └── pre-commit # GIT HOOK, runs on every commit, staged files only:
│ # RuboCop (incl. 34 custom cops), Herb linter (incl. 2 custom
│ # rules), Prettier on Markdown, and the intent/spec/plan linters
├── .rubocop.yml
├── lib/rubocop/cop/boswell/ # 34 CUSTOM COPS, e.g.
│ ├── redirect_see_other.rb # 303 after a non-GET request
│ ├── no_shard_switch.rb # one database per tenant: no raw shard switch
│ ├── controller_authorizes.rb # every action authorizes (Pundit)
│ ├── no_query_in_view.rb # no database query in a view
│ ├── no_form_with.rb # simple_form only
│ ├── flash_from_i18n.rb # flash text comes from the language files
│ └── ... # (job time zone, REST-only routes, test sleeps, ...)
├── .herb.yml
├── .herb/rules/ # 2 CUSTOM HERB (ERB/HTML) RULES
│ ├── boswell-no-literal-text.mjs # no typed-in text in a view: use t()
│ └── boswell-turbo-stream-no-markup.mjs
├── config/
│ ├── ci.rb # bin/ci: every check, once per head, before merge:
│ │ # Style (RuboCop, Herb, Markdown, CSS tokens),
│ │ # Security (Brakeman, importmap audit),
│ │ # I18n (i18n-tasks), tests, and tests of the hooks themselves
│ └── i18n-tasks.yml
├── docs/standards/ # the written rules that remain: 12 files, 662 lines
├── .claude/
│ ├── settings.json # wires the CLAUDE CODE (harness) HOOKS
│ ├── hooks/
│ │ ├── credentials-guard.rb # PreToolUse Bash: refuses any command that could read secrets
│ │ ├── spec-lint.rb # structure of a spec } also run by the git pre-commit hook
│ │ ├── plan-lint.rb # structure of a plan }
│ │ ├── intent-lint.rb # structure of an intent }
│ │ ├── build-context.rb # SubagentStart: hands each agent its own build's facts
│ │ ├── runner-record.rb # runs a workflow's shell commands exactly as sent, records output
│ │ └── step-record.rb # PostToolUse: records each build step from the agent's result
│ ├── agents/ # one job each: coder, build-verifier, triager, qa,
│ │ # architect, product-manager, runner, ...
│ ├── skills/ # e.g. boswell-build, boswell-plan, boswell-qa,
│ │ # boswell-code-writer, boswell-testing, boswell-issue
│ └── workflows/
│ ├── boswell-build.js # the build loop, a multi-agent script
│ └── standards-eval.js
└── bin/ci, bin/rubocop, bin/brakeman
There are two kinds of hooks here. The git hook blocks a bad commit, from a person or an agent. The Claude Code hooks act on the agents: they refuse access to secrets, hand each agent the facts for its build, and record each step from structured results, so no model writes the record. Scripts make every rule-based decision in the build, like what step is next, whether CI passed, and whether it may merge. The models write code and make judgment calls.
Target practice
Whack-a-mole is a reaction game. A mole pops up somewhere new and you swing. In target practice, the targets stay where you put them, and every shot gets scored.
Now the checks run every time an agent commits. When one fails, the commit stops and the message names the rule and the fix. The agent fixes the line and commits again, with no reviewer involved:
agent writes code ──► git commit
│
pre-commit hook runs the cops on staged files (seconds)
│
┌─────────────┴─────────────┐
pass fail
│ │
commit lands commit refused; the cop prints its rule, e.g.
"Add `status: :see_other` to `redirect_to` in `destroy`.
A redirect after a non-GET request uses 303 ..."
│
agent fixes the line and commits again
(no human, no reviewer, no tokens spent judging it)
That frees the rest of the build loop for judgment. Verify checks each acceptance criterion, review reads the code, and QA walks the critical path a user takes across every area the change touched. Their findings are triaged together: fix now, file an issue, or discard. Fixes go back to a writer and get checked again. Then CI runs everything again before a scripted merge:
spec + plan
│
▼
┌────────┐ commit ┌──────────────────────────┐
│ write │──────────►│ git pre-commit hook │
│(coder) │◄──────────│ RuboCop + 34 cops, Herb, │
└────────┘ refused, │ Prettier, spec/plan lint │
│ with the └──────────────────────────┘
│ rule's message
▼
┌────────────────────────────────────────────┐
│ verify: each acceptance criterion │
│ delivered? pinned by a test? │
│ review: code review │
│ QA: walk the user's critical path │
│ across the changed areas │
└────────────────────────────────────────────┘
│ findings from all three
▼
triage (fix-now / issue / discard)
│ │ fix-now
│ └──────► back to a writer, then checked again
▼
push ──► bin/ci (all checks + Brakeman + tests) ──► posts boswell/ci
│ │ fails
│ └──────► back to a writer with the failure
▼
boswell/ci + boswell/review both success ──► merge (scripted, not a model's call)
This week’s Spanish work is a good example. Every word on a page has to come from the language files. A Herb rule flags any word typed straight into a view, so the agents moved the text where it belongs and nobody had to remind them.
A cleaner kitchen
As the checks grew, the written rules shrank. At the end of September, docs/standards/ held 38 files and 4,062 lines. Today it’s 12 files and 662 lines. The checks weren’t the only reason, since we also merged and cut files. But rules like the 303 redirect no longer appear in the written standards at all. They live only in the cop, and its message names the fix.
My reference point is 37signals, whose written conventions are short. Boswell’s may never get that short. But the more a rule is a check that a hook enforces, the less an agent has to read and remember. The gap closes faster now.
Next: a better first shot
Linters catch a miss after the code is written. Next I want the agent to start closer to the target. Rails lets you write your own generators, so the next step is a few of those. Someone in my spot might make generators for:
- a controller
- a model, with the right base class for customer data or shared data
- a background job that runs against the right customer’s database
- a Pundit policy
The controller is the one I’d start with. When an agent needs a new controller, it would have to run the generator. The generator would include the needed modules, inherit from the right base class, add the Pundit authorize call, and leave empty methods with comments on what to fill in and what to delete.
I haven’t built them yet, but I expect a solid starting point to leave the agent less to figure out on its own.