LaunchKit
← All posts
· 14 min read · by The LaunchKit team · 1 views

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.

#rails #architecture #testing

Comments

No comments yet. Be the first.

Only used to confirm and publish your comment. Never shown publicly, never shared.

Markdown: **bold**, `code`, ```fenced blocks```, > quotes, [links](url). HTML and images are not rendered.