Layers: an AI SKILLs Pack

Whether you’re directing your AI or working alongside it, Layers walks you both through all seven layers of product design — so the decisions underneath the screen actually get made.

We’re definitely in it, an AI-assisted world where skills packs like this are being created to help build products. This does look cool, though, and the site design is noice. I’m a sucker for scribbled/hand-drawn-esque artwork.

Another hours long conversation between Lex and DHH

I don’t know how I end up watching (or, listening) to these billion hour interviews, but they’re nice background noise while working. It’s quite the contrast between the first interview and this one, with “agentic engineering” being the primary topic. It’s ineresting to hear DHH come around on AI and actually praise what it provides. He’s a love or hate kind of personality, but regardless, I do think he’s one of the best programming minds in the world, so it’s pretty interesting to hear what he has to say about all of this.

Thoughts on AI SDLC

I’m blown away we’ve been able to make computers do this. I know it’s “just” algorithms predicting the next token… it could be a word, an exclamation point, or a “haha”. It’s not actually alive, Ryan, right? Right?? It sure as hell feels like someone is on the other end. But that’s the magic. The conversational back-and-forth is what sells you on the future. Of course every input will just be natural language, why wouldn’t it be? When you can speak freely to someone something that happens to know everything or is smart enough to figure it out, doors open up.

But it’s not all rainbows and ice cream. We’re still early on this journey and I have mixed feelings, at least when it comes to the software development lifecycle.


Only Programmers are Programmers

The digital age has put programmers in high demand for a couple of decades now. They were the ones who could turn your idea into something real. Entire businesses are built around teams of programmers making software to satisfy a need. I know, because I’m 25 years into this industry. The promise of AI flips all of this on its head: now anyone can be a programmer!

There’s certainly truth to that, but do we really think that AI levels the playing field so much that decades of prior experience no longer matters? I’m not so sure. Even if AI is doing the heavy lifting, wouldn’t my ideas, questions, and guidance be more effective than, say, my mom’s? Surely it would!

It can definitely deliver results in all cases, but it still feels like I’m seeing far better AI output from programmers over designers, product owners, project management, etc. And I have to think that’s because of who’s in the driver’s seat.

Code is Art

When I’m writing code, I’m also focused on the elegance of programming. Coming up with clever ways to build things is addicting. The problems often weren’t the hard part, the challenge was solving them in the cleanest, most simplistic way. Sure, I bet that led to a bit of overengineering in my day, but the point is I cared deeply about code quality and the act of producing it. It’s why programmers like to write “idiomatic” code in their language of choice. The non-idiomatic version still works, but there’s way less satisfaction.

Manually working through business logic/requirements, designing a scalable data model, writing shared modules/packages/plugins, crafting APIs… it’s a beautiful and rewarding experience. With AI, context bloat is a real problem and it makes choices based on what it can reason about within that context window. Hallucinations happen and sometimes the quality slips. Ultimately, the sad reality is that AI demands I trade productivity for the creativity, art, and joy of programming as it once was. Maybe it’s nostalgia, but I already kinda miss it.

Speed, Speed, Speed

In golf, you don’t pick up club speed from swinging as hard as you can. In fact, it’s often the opposite. Improved technique is what gets you that bump in speed. You don’t have to improve your technique and swing as hard as you can. It’s counterproductive.

Enough metaphors. In software, the speed boost is real, but it’s not enough! A 30% boost in speed could be a 50% boost in speed if we can crank out code reviews even faster! AI can write your commits! AI can create the PRs!

We need to take the gains where they come, where it makes sense and not force all things, in all ways, to adopt AI because of speed, speed, speed. Keep the best practices you’ve learned in place and utilize AI to improve your technique. Iterate your way through it.

Anyone Know How This Works?

The really good programmers know how their stuff works. What I’m seeing more and more are engineers who are a little unsure. The PRs are written with AI and code review questions are answered with AI. So who’s the expert? Maybe I’m being grumpy about this, and the actual answer is: “who cares, because Claude will fix the problems, anyway”.

As long as there are on-call rotations in software, I’m really hesitant to let that human knowledge start fading away.

Spec-Driven Development

We’ve been building features with SDD for most of 2026 now. It’s a powerful tool in the toolkit. Planning, speccing, and tasking the work into phases is exactly how you get reliable results with very limited hallucinations. Our tooling allows us to build features across many repos. We orchestrate a swarm of agents to do planning, implementation, code reviews, and testing. Certain models are good at certain things. We take advantage of that.

But.

This is a big workflow with a lot of artifacts to review before any agents do work. Not all features are big features. So we’re still hunting for the middle ground, because most of the time this feels a little heavy-handed, somewhat overengineered.


Let’s reset. That all felt a little negative… what do I like about this brave, new world?

Refactoring Made Simple

When you work on software with a lot of contributing teams it’s inevitable that you’ll end up with things that should’ve been done better. A decision deemed temporary is now baked in pretty deep. And with a handful of Product folks sorting the priorities, going back to fix it never makes the list.

But now, you can spawn a team of agents to untangle that mess. Tech debt is often ugly, but it usually comes with one benefit: you know what’s wrong with it and you know how to improve it. That’s a great recipe for steering AI toward good outcomes.

Exploration

Before AI, the ideating often happened in a proposal. It was theoretical, “here are the options”. But now? You (or “you”) can build proof-of-concepts in a matter of minutes. I’m old school, so I often like to design in the browser. It always made sense to me to explore ideas under the constraints and limitations of the stack, on-screen rendering, available components, etc.

AI makes this workflow so painless and you get to feel what customers will experience. This was always a much more effective way to explore ideas, it was just too expensive before. This is probably my favorite thing the technology affords.

Planning

A main way I use AI is to help me plan or ideate on a feature. As I mentioned above, these are those magical moments when I get lost in “conversation” and forget I’m not talking to a colleague (let’s ignore the societal impacts of what I just said, though). Tossing out ideas that push past what you’d normally consider is useful, and AI makes that really easy to do.

Of course I don’t blindly trust the output, but I do find value in sanity checking my ideas or approach against something that can look into every dark corner of the codebase and help keep me in check.

Gone is the Mundane

Easy and boring things are just gone. It started with automated tests, you get those for free now. But aside from that, there are many parts of a feature or application that you work through on your way to the good stuff. Often, you know exactly what needs to happen on those boring parts, and AI can really shine when you give it clear direction. It frees me up for the more interesting parts of my job, and I appreciate that.

MCP & SKILLs

MCP is a protocol that many services support. It expands what your AI knows and what it can do by tapping into third-party systems you use. AI interactions can be pretty great regardless, but when they can see beyond that baseline, it really is a game-changer.

SKILLs are agent instructions for specific—perhaps routine—things. You define a scoped set of context, tell the agent how to behave, describe when and why to use it, and the AI decides when the time is right during normal interactions. You can also call them explicitly, of course.

I’ve found that a solid CLI + SKILL combo is as good or better than MCP integrations, and they’re often easier on the token consumption, but that’s for another post. All of this is to say that you have to take the time to properly hone your setup for the best experience, and MCP / SKILLs are a huge part of that.

Conclusion

Admittedly, I struggle to hand full control over for AI to go crazy and build my feature. That doesn’t mean I don’t rely on AI routinely (I do), I’m just more intentional about when I bring it in. It’s AI-assisted development. I’m in control, I guide it, I determine what I don’t care to write and what I do. I still focus on small PRs, small changesets. I think many people approach it this way, but definitely there’s a camp of people whose AI trust meter is maxed out and they set it loose on their code. I’m not there yet.

I don’t know where all of this is going to go. I don’t know what it’s going to mean for the future, both in tech and for my kids, but we have a technology that’s fundamentally changing everything, and it’s improving by the day. Natural language is the future interface.

We like to complain and throw our hands up at the faults of AI, humans (engineers especially!) are hilarious in that way. But I do think it’s worth taking a moment to marvel at where we are. It really is incredible. And scary. We’ll see!

Onward.

Changing Views on AI

I’ll admit, I spent a long time criticizing AI tooling, perhaps a bit disappointed with what the future looked like. I know many people who were ecstatic about all of this, all-in from day one. But it always felt different for me. Part of my love for programming came from designing and writing beautiful code. Or at least, striving to design and write beautiful code. Most of my career I was reaching for that “craftsman” status. I believ(ed) in that. Code is creative. Code is art.

Then came AI. In the beginning it was laughable, zero threat. But it was clear where we were headed. As it got better and better, it began trying to rip that “craftsman” mindset away from me. It’s hard to pinpoint what I was bitter about, though. Was it because it (seemingly) “leveled the playing field” so all of the decades of hard work I had put into building software no longer mattered? Maybe. I realize that’s a goofy way to think, but I take a lot of pride in my work, and pride is funny like that.

But—sometime last year I moved through the stages of grief and started accepting my fate. This was the way. So at the beginning of 2026 I really embraced what it could do for me. I tried to think “AI-first” with most things I had to do. Lots of experiments, running local models, lots of reading, learning, and adapting. I was a tinkerer again.

The result? It kinda paid off. I have workflows now that I couldn’t imagine doing manually anymore.

I think my fears and dread all stemmed from AI replacing things. If I no longer designed the solutions to solve my problems, then what am I? Just a “prompter”? Blah. But that turned out to be so far from the truth, at least for me. What I realized very quickly is that an AI-assisted workflow provided peak productivity gains and still left me with all the control I wanted. And a bonus on top of it all: the better the engineer, the better the AI output. Which means experience, knowledge, preferences… all still matter.

While I’m still unsure how happy I am about the future, it’s at least a lot less bleak. Engineering is still about best practices, tradeoffs, and adapting to the ever-changing shifts in technology. This particular shift just took a lot of intentional effort and acceptance before I could thrive in the new world. But I’m here now, embracing it, keeping my fingers crossed along the way.

Querying APIs with Vanilla JS

I’m at the point in my career where keeping things simple is a top priority, even if that means a little sacrifice in other areas. In this particular case, as part of redesigning rpheath.com, I wanted to pull in the latest N posts from this blog. Given that I’m working with a static site, that meant using JavaScript. A young whippersnapper may immediately turn to React, Vue, Svelte, or (gasp!) Angular, which is reasonable. Those frameworks make this type of thing clean and easy. But I did not want to deal with integrating webpack and/or babel into my builds, as I’ve been down that road plenty, and it’s (shockingly!) still pretty annoying to deal with. Besides, each redesign of my personal site strives for more and more simplicity, so that would go against the grain.

I chose to use vanilla JS for this. Let’s talk about it! It all starts with a fetch-based function to grab the data from the blog’s API:

1function fetchPosts(url, callbackFn) {
2  fetch(url)
3    .then(response => response.json())
4    .then(data => callbackFn(data))
5    .catch(error => console.error(error));
6}

The fetchPosts() function grabs data from an API and hands it over to a callback function that I define for processing. This is all so simple because fetch() is built on the Promise, which in modern JavaScript, is an expectation for asynchronous calls.

Since we don’t have a framework to do stuff for us, the next “problem” we need to take care of is ensuring that our code runs at the right time. Meaning, after the DOM has loaded. Old school folks may remember jQuery’s $('document').ready(...) or $(function() { ... }) helpers! Yeah, it’s that, but with vanilla JavaScript. There are two ways to approach it:

 1// (1) wait for the DOMContentLoaded event
 2document.addEventListener("DOMContentLoaded", (_e) => {
 3  console.log("The DOM is ready!");
 4});
 5
 6// (2) a self-executing function
 7(function() {
 8  console.log("The DOM is ready!");
 9  console.warn("...but only if this JS is included at the bottom of the page");
10  console.warn("...also, I ignore deferred scripts!");
11})();

Both work, but notice the caveats with option (2) above. If you go with a self-executing function, it’ll execute whenever the browser reaches it, so it must be after your HTML is parsed, top-down (and even then it still ignores deferred scripts). We’ll go with option (1) for our purposes.

The next bit to work out is what to do with our data once we get it back from the API. In React, for example, we’d make a component that spit out some HTML for the browser. Well, that’s exactly what we’ll do, only we’ll use JavaScript’s Template Strings instead.

 1const renderPosts = (data, container) => {
 2  let posts = [];
 3
 4  data.forEach((post) => {
 5    const html = `
 6      <div class="post">
 7        <div class="post-date">
 8          <time>${post.date}</time>
 9        </div>
10        <div class="post-link">
11          <a href="${post.url}">${post.title}</a>
12        </div>
13        <p class="post-caption">${post.summary}</p>
14      </div>`;
15
16    posts.push(html)
17  });
18
19  container.innerHTML = posts.join("\n");
20};
21

I bet any youngsters who stumble onto this won’t even know what innerHTML is. But essentially, we just take the data and iterate over the response, creating an array of string-interpolated items. Once we’re done “looping”, we just set the container’s HTML to render our posts.

It’s a nice touch to put some kind of default text or loading indicator into your “container” element:

1<div id="blog-posts">
2  [spinner svg] Loading Posts...
3</div>

Then once the DOM loads and your function executes, innerHTML will replace your loading content with the interpolated response from the API.

Now that all of our pieces are in place, the result looks like this:

1document.addEventListener("DOMContentLoaded", (_e) => {
2  const blogContainer = document.querySelector("#blog-posts");
3
4  // we only want to do this for the "blog" section of the site
5  if (blogContainer) {
6    fetchPosts((response) => renderPosts(response.data, blogContainer));
7  }
8};

You can see this in action by going to rpheath.com/blog.

The important lesson here, if there is one, is to avoid defaulting to complexity because “that’s what the internet does”. If I wanted to use React for this, I totally could, and the outcome would look very clean and satisfying. But the complexity involved with integrating React into my builds was just not worth it. I have the same result, but with zero configuration or headache. I’m just using what browsers already know and understand. And for me, that’s the reward.

The Proactive Engineer

I’ve been a young engineer, mentored young engineers, and now I manage (some) young engineers. One of the best perspectives a young engineer—or any engineer—can have is being proactive.

Engineering is complicated. There are often multiple teams and departments coinciding with ever-changing requirements. This can provide engineers with a tempting out (or “pause button”) when working on a problem because they feel blocked on some level. Does something like this sound familiar?

We can’t work on that yet because there’s no API.

Quite often, though, there’s more an engineer can do than claiming blockers. With minimal additional effort, let’s see how much more helpful a response like this might be:

We can’t work on that yet because there’s no API. But I spent some time thinking about what inputs and outputs we would need to properly build X feature, and wrote up my notes as a proposal for the backend team’s consideration.

Makes a difference, right?

It’s easy to throw things over the fence. It takes intentional effort to avoid it. But when you do, that proactive mindset can go a long way within a team and helps nurture a problem solving culture. It’s not about you, me, or them. It’s about solving a problem and helping everyone involved come up with the best outcome for the situation.

We all want to work with helpful, caring individuals… a proactive brain can help convey that to others.

How to continue making kerosene lamps on the eve of electricity

I don’t think I fully believed in AI building software until fooling around with ChatGPT. Maybe it won’t affect software development like it feels like it might, but I don’t think it can be ignored any longer.

I like to imagine that all of us in software development, as it looks today, are busy making beautiful kerosene lamps in all shapes and sizes. We’re improving the burn efficiency. We’re finding ever clearer forms of glass to let the light through. We’re tinkering with a formula that’s been around for a long time. But as we do, Edison and Tesla are busy inventing electricity in the other room.

Setting Ownership in Rails

There’s a common pattern in a lot of applications to track who did what. Who created this post? Who was the last person to update it? Maybe it’s for accountability, maybe it’s because your client is nosy.

There are gems for this, but it’s such a simple thing, that extra dependency isn’t really worth it. So let’s take a look at a pretty simple way to achieve this in Rails 7 using a concern:

 1module Ownership
 2  extend ActiveSupport::Concern
 3
 4  included do
 5    before_validation :set_ownership
 6
 7    scope :created_by, ->(user_id) {
 8      where(creator_id: user_id)
 9    }
10    scope :updated_by, ->(user_id) {
11      where(updater_id: user_id)
12    }
13
14    belongs_to :creator, class_name: 'User',
15      foreign_key: 'creator_id', optional: true
16    belongs_to :updater, class_name: 'User',
17      foreign_key: 'updater_id'
18
19    validates :creator_id, :updater_id,
20      presence: true
21  end
22
23  private
24
25  def set_ownership
26    current_user = Current.user
27
28    if new_record?
29      self.creator_id = current_user.id
30      self.updater_id = current_user.id
31    else
32      self.updater_id = current_user.id
33    end
34  end
35end

Now, for any model that has creator_id and updater_id columns, you just have to include the Ownership module:

 1class Post < ApplicationRecord
 2  include Ownership
 3
 4  # ...
 5end
 6
 7>> @post = Post.find(1)
 8>> @post.creator
 9# => #<User id: 10 ... >
10>> @post.updater
11# => #<User id: 20 ... >

And whenever a Post is created, the creator is automagically set; similarly, whenever any Post is updated, the updater is automagically set. This, of course, assumes your application has user sessions, since you obviously need to know who’s logged in doing the creating and updating.