DHH let designers vibe code Basecamp 5, and the PRs destroyed the architecture
The agent failure DHH describes in the most detail is not in the Rails World keynote. It sits at 00:16:44 of a podcast published four weeks earlier, and it is the one part of the story a Rails team can learn something from:
And we had this early surge of, "It's solved. We can just have the designers do the programming. They know what features they want. They know what shape they want it to take. Let them vibe." And we let them vibe. And we ended up with a lot of PRs that individually perhaps could have been justified for a hot moment, but taken all together, destroyed the architecture of the system. And we actually had to clean up manually, mop it up by hand, by human hand, to get back to an architecture that felt cohesive and coherent.
His next two sentences are part of the quote and belong in any honest use of it: "that was February, by the way. Things are quite different now." He is describing a failure he considers dated. Read the failure anyway, because the shape of it does not depend on how good February's models were. Every PR passed review on its own. The damage only existed in the sum.
That is a property no tool in a normal Rails repository measures, and the rest of this page is an attempt to measure it on one. Everything below was run on an Apple M2 Max, 12 cores, macOS 26.5.1, against Ruby 4.0.5 and Rails 8.1.3.1, with the command that produced each figure named beside it.
The episode, and why the date matters more than the quotes
Lex Fridman Podcast #501, "DHH: Future of Programming, AI, Agentic Engineering, Vibe Coding & Linux", was published on 2026-08-26 and runs to a final transcript timestamp of 05:15:15. The transcript is on Lex Fridman's site with clickable timestamps, and the video is youtube.com/watch?v=NYFGCESmikA. Every quotation on this page was read there.
The Rails World keynote that produced "pencils down" was on 23 September 2026, and what DHH actually said at Rails World takes that apart separately. The useful thing about the podcast is that it predates the keynote by four weeks and is five hours long, so it contains the part a keynote has no room for: the failures, the boundary he actually reviews to, and the exact shape of his daily workflow.
Three of the timestamps were one speaker turn off
Verifying a quotation against a transcript is not optional work on this subject, because the quote being right and the timestamp being right are two different things, and a reader who clicks through only checks the second one. Ten timestamps were handed to this page from an earlier reading. Three of them land on the wrong turn:
- "Write it like DHH would" sits at 01:07:31, not 01:08:01. The 01:08:01 turn begins "And they're pleased with the result". Click the later stamp and the sentence you came for is above the fold you landed on.
- "I have not written any of the code that's shipped in Quattro by hand" is not one turn. "I have not written" ends 00:15:14. Lex says "Really?" at 00:15:42. DHH resumes at 00:15:43 with "... any of the code that's shipped in Quattro by hand."
- The Copilot half of his workflow is at 02:38:47, not 02:38:14.
One more is not a timestamp error but a sourcing one. "100% one person can" at 00:22:27 is DHH's complete turn. The subject of the sentence, Premiere and Photoshop for Linux, is Lex Fridman's, from the question at 00:22:17. Quoting DHH as claiming one person can build Photoshop puts a proposition in his mouth that he agreed with rather than made.
Two of those three were marked verified by the previous reader. That is the entire argument for doing it again: a correct quote with a wrong timestamp is indistinguishable from an invented one at the moment somebody checks, and checking is the only thing this kind of page is for.
The boundary he says he reviews to
At 00:15:43, describing Omarchy Quattro, DHH draws a boundary with three tiers in one sentence:
I've reviewed the shape of all of it. I've reviewed the individual lines of anything that's critical in the model layer of the system, and I've not looked at a bunch of the UI code.
Shape for everything, lines for the model layer, nothing for the UI. It is a real answer to the real question, which is not "should you review agent code" but "you cannot read all of it, so what do you read". And it is stated by somebody who then, in the same breath, says the web products were the ones that went wrong.
Pricing that boundary on a real Rails codebase
bin/rails stats on this repository, which is the sales site behind launchkit.codes:
| Layer | LOC | Classes | Methods |
|---|---|---|---|
| Models | 2,461 | 80 | 483 |
| Controllers | 1,878 | 65 | 247 |
| Views | 9,675 | 0 | 1 |
| Helpers | 414 | 0 | 61 |
| Jobs | 100 | 5 | 9 |
Code LOC 18,026, Test LOC 11,367, ratio 1:0.6. Apply DHH's boundary and 2,461 of 18,026 lines get read individually, 13.7 percent, while 9,675 lines of ERB get the shape treatment and nothing else.
That percentage is wrong, and it is wrong in the direction that matters. bin/rails stats does not
count app/services. Rails::CodeStatistics::DIRECTORIES in railties 8.1.3.1 is a fixed array whose
production half is twelve app/ paths plus lib/. It includes app/apis, a directory almost no
Rails 8 app has, and excludes app/services, app/forms, app/validators and app/serializers,
which this one has. Measured with Rails' own CodeStatisticsCalculator so the arithmetic matches:
app/services lines 1981 LOC 1239 classes 44 methods 181
app/forms lines 152 LOC 111 classes 7 methods 17
app/validators lines 30 LOC 17 classes 1 methods 1
app/serializers lines 27 LOC 22 classes 1 methods 2
app/models lines 4699 LOC 2461 classes 80 methods 483
The app/models row reproduces bin/rails stats exactly, which is how you know the other four rows
are comparable. So 1,389 LOC, 53 classes and 201 methods of application code are missing from the
18,026. The real denominator is 19,415, and the model layer is 12.7 percent of it.
The funny part is that the same command already knows. bin/rails stats on this app prints a
Service specs row at 1,277 LOC, a Form specs row at 46 and a Validator specs row at 50,
with no Services, Forms or Validators row anywhere above them. That asymmetry comes from
rspec-rails 8.0.4: spec:statsetup in lib/rspec/rails/tasks/rspec.rake walks the spec/*
directories and pushes ["#{name} specs", dir] onto STATS_DIRECTORIES at runtime, while the
production side of the table is the frozen array in railties. The output tells you there are 1,277
lines of tests for a layer the same table says does not exist.
Static LOC is also the wrong unit, because review reads diffs. Over the last 200 commits on this
repository, 2026-08-16 to 2026-09-26, counted with git log -200 --numstat --pretty=format: --
'app/*':
app/views +12281 -1486
app/models +4834 -135
app/controllers +3147 -108
app/services +2044 -63
app/helpers +721 -23
Views took more added lines than every Ruby directory under app/ put together, which came to
11,269. The median commit touching app/ added 39 lines, the 90th percentile added 820, and the
largest added 16,766. The tier DHH skips is the tier that moves most, and that will be true of any
server-rendered Rails application, which is most of them.
What no check in this repository can see
Looking for the mechanical guard that would catch the Basecamp failure mode here produced nothing, and the nothing is worth printing because it is so emphatic.
bin/rubocop on the whole repository: 558 files inspected, no offenses detected, 1.60s wall with
rubocop 1.91.0 on rubocop-rails-omakase. bin/rails zeitwerk:check: "All is good!", 1.109s.
Both of those are true of a codebase where the same class exists twice under two names. To check
rather than assume, app/services/comment_markdown.rb was copied to a scratch directory, once
verbatim and once with class CommentMarkdown rewritten to class CommentRenderer, and linted with
this project's own configuration:
$ bin/rubocop --force-exclusion /tmp/.../dup
Inspecting 2 files
..
2 files inspected, no offenses detected
One hundred lines of identical logic, two files, zero findings. That is not a RuboCop defect. RuboCop is a per-file linter and the Basecamp damage was a property of a set of PRs, which no per-file tool has standing to notice. The dead end is worth keeping in mind before anyone proposes to solve agent-written architecture drift by turning on more cops.
The full suite is the only thing left. bundle exec rspec here: 1,687 examples, 1 failure, 2
pending, 32.95 seconds with files taking 1.79s to load. Thirty-three seconds is cheap enough to run
on every agent branch. The question is whether it asserts anything an agent could break by being
reasonable.
The one thing that did go red
app/services/comment_markdown.rb is a 100 line class that renders Markdown, and MarkdownRenderer
is a 90 line class that renders Markdown. Any reviewer, human or otherwise, would flag that as
duplication. The file opens by arguing against itself:
# Renders UNTRUSTED Markdown - reader comments - to HTML.
#
# Deliberately NOT MarkdownRenderer. That one is for our own Yield articles and quiz answers:
# copy we wrote, where a permissive renderer is fine. A comment is written by a stranger, so this
# is a separate, locked-down configuration and the two must never be merged "to avoid duplication".
# The duplication IS the safety property.
So: does anything mechanical hold that line, or is the comment the whole defence? The PR an agent
would open was simulated without editing a repository file, by prepending a module that makes
CommentMarkdown.render delegate to MarkdownRenderer.render and loading it into RSpec with -r:
RSpec.configure do |config|
config.before(:suite) do
CommentMarkdown.singleton_class.prepend(Module.new do
def render(text) = MarkdownRenderer.render(text)
end)
end
end
21 examples, 12 failures
rspec './spec/services/comment_markdown_spec.rb[1:2:1]' # CommentMarkdown link targets refuses to link javascript
rspec './spec/services/comment_markdown_spec.rb[1:2:3]' # CommentMarkdown link targets refuses to link data
rspec ./spec/services/comment_markdown_spec.rb:67 # CommentMarkdown images makes no image request on the reader's behalf
rspec ./spec/services/comment_markdown_spec.rb:73 # CommentMarkdown headings demotes h1 so a comment cannot outrank the page title
Twelve of twenty-one, in 0.024 seconds of example time. The merge is caught, and it is caught loudly enough that nobody could plausibly delete the failures to make the branch green: the first line of the output is a stored XSS.
Read why it worked, though, because the mechanism is narrow. Those examples pass not because
anything in the repository knows the two classes must stay apart, but because
spec/services/comment_markdown_spec.rb opens with a comment saying "each one names the attack it
closes, because a future contributor tempted to merge this with MarkdownRenderer ... needs to see
exactly what they would be turning off", and then somebody wrote refuses to link javascript as an
example name. An agent that collapsed the two classes while preserving every Redcarpet option would
have left all 21 green and taken the architecture with it. The suite caught a behaviour that happened
to encode the architecture. It has no opinion about the architecture.
His procedure, which is the part worth copying
At 02:38:14 DHH describes what he actually does, and it is the most directly reusable thing in five hours:
I'll have a Opus or Fable do the work, and then I always end it, review with Codex xHigh.
At 02:38:47 he adds the second gate: he pushes to GitHub and Copilot reviews the branch, and "Copilot keeps finding stuff that's legitimately broken". At 02:39:24 he gives the reason, which is the least mystical sentence in the episode: "if you finish a job and you ask your also very good peer to review it, you're gonna end up with better code. Of course you're gonna end up with better code. So build that into your process."
Three reviewers, two of which did not write the code, one gate on the developer's machine and one after push. There is no claim in there that the models are correct, only that independently sourced ones fail differently. That is a cheap thing to adopt and it needs no new infrastructure.
What this changes on Monday
The position here, stated so it can be argued with: copy 02:38:14, do not copy 00:15:43.
The procedure is portable. A second frontier model reviewing the first one's diff before it leaves your machine costs one command and finds real defects, and DHH is describing months of production use rather than a demo. Adopt that this week.
The boundary is not portable, and the measurements above are why. "Line-review the model layer, shape
the rest" is a sensible rule for Omarchy, where the model layer is the system and the UI is a
terminal. On this codebase it puts 12.7 percent of the application under line review while the
directory taking the most churn, app/views at 12,281 added lines across 200 commits, gets read for
shape. DHH said the same thing about his own web products at 00:15:43: Basecamp and HEY "have proven
surprisingly tricky to fully accelerate with agents". The boundary that works on the Linux
distribution is the one he reports struggling with on the Rails apps, in the same answer.
What would change the position: if your views are genuinely presentational, because rendering has moved to native clients or you are shipping an API, then the model layer really is the system and the boundary is correct. That is precisely the HEY 2.0 shape, and it is the reason the boundary and the rewrite were announced by the same person. Get there and adopt the rule.
The cost of not adopting it is honest to state, because the alternative is not free. Reviewing views
line by line means reading 9,675 lines of ERB where most diffs are class names and spacing, and
attention spent there is attention not spent on the 2,461 lines where the wrong answer is silent.
A search of this repository's 226 ERB files found admin? twice and nothing else that gates
visibility, so the "authorization hides in your views" argument does not hold here and is not being
made. The argument being made is smaller and harder: layer is the wrong axis. Draw the boundary
around the properties you cannot afford to lose, write the spec that names each one the way
comment_markdown_spec.rb does, and let the agent have the rest.
What this post does not cover
Not the other four and a half hours. Lex Fridman Podcast #501 runs past five hours and this page quotes from six of its chapters. Omarchy, Linux on the desktop, fatherhood, immigration and the longevity material are all in there and none of it is characterised here.
Not the Rails World keynote. That is a separate page, written from the posts and the session listing, and nothing here re-argues it.
Not a verdict on any model. DHH names Opus 5, Fable, Codex xHigh, Grok and Copilot at 02:38:14 and after, and no comparison of them was run for this page. His ranking is his.
Not the economics. At 01:14:04 he gives the Jevons argument, "Productivity means fewer people to do the same number or the same job. Now, the amount of job you want done may increase", and at 01:10:51 "there's a great argument for us needing far more builders than what we have now". Both are claims about a labour market that nothing in a Rails repository can measure, so nothing here tests them.
Not a rubocop custom cop for cross-file duplication. It may well be buildable. Nothing of the kind was written or run here, and the 558 file, zero offense baseline above is the only claim being made.
Comments
No comments yet. Be the first.