• The REPL: Issue 136 - December 2025

    RFD 576 Using LLMs at Oxide

    A thoughtful overview of how Oxide uses AI. It breaks down values, different uses, and reaches solid conclusions. It recognizes AI’s usefulness for some cases while keeping humans firmly responsible.

    Has the cost of building software just dropped 90%?

    AI’s explosion generates more demand for software by making it easier to build. A developer and business analyst can now ship an incredible amount at lower cost. What once required five people working for months now takes one developer and a part-time business analyst a week. Because creating internal tools and prototypes is cheap, more will get built.

    Smaller teams save money another way: less coordination. Fewer status meetings. Of course, developers now coordinate agents instead.

    I wonder whether ideas will be less polished when fewer people are involved before building starts. As Tim Cook says:

    You can go fast alone. You can go far with a team.

    Building the right thing still matters. “Because we can” is not a reason to build something.

    Horse

    The horse and chess analogies are interesting. They suggest that as new technology improves incrementally (~20% per year), it reaches a tipping point where it clearly becomes better than the older technology, and adoption is immediate.

    Cars improved steadily until they were clearly better than horses. On the next “replacement” cycle, most people traded their horse for a car.

    Chess programs improved beyond a threshold where they could beat a human most of the time.

    Now, regarding jobs and AI: answering questions from a knowledge base is a task where AI excels. As the author acknowledges, that part of his job is no longer something he does. He was “replaced” – but that was only part of his job. In fact, that wasn’t even the principal part:

    I was one of the first researchers hired at Anthropic.

    Presumably, everyone is happier that he can now focus on actual research instead of answering onboarding questions.

    My takeaway: AI is already changing some of our tasks and changing our jobs.

    Prediction: AI will make formal verification go mainstream

    I’ve always been interested in formal verification. The idea of mathematical proofs for algorithms and software is appealing. As noted, it is highly impractical because writing the proofs themselves is hard and requires “PhD-level training”.

    Kleppmann argues that AI can change that, and not only that, can help validate the onslaught of AI-written software that is coming.

    Read on →

  • The REPL: Issue 135 - November 2025

    LLMs are steroids for your Dunning-Kruger

    LLMs can give you false confidence, because they can reinforce your prior beliefs. They are “eager to please”. Asking leading questions can change the results greatly. I am mindful of asking questions that are neutral.

    Using the expand and contract pattern

    A good overview of the expand and contract pattern. Handy to keep around to send to others, which comes up about once a month!

    We should all be using dependency cooldowns

    This is the first time I’ve heard “dependency cooldown” as a term. In a sense, I already implement the cooldown instinctively. I upgrade methodically and purposefully, especially when major versions are concerned.

    Read on →

  • Git: Amend any commit

    I recently read The (lazy) Git UI You Didn’t Know You Need and experimented with lazygit. My usual workflow involves invoking git directly, staging commits with git add and git add -p, often selecting files using a fuzzy finder. The article highlighted lazygit’s ability to amend any commit, not just the most recent one, which sparked an idea.

    Current Workflow for Amending Commits

    To amend a commit, I first stage the changes. Then, when I want to amend the last commit, I use:

    $ git commit --amend
    

    This opens my editor with the previous commit message, which I usually accept and close without change.

    Amending a previous commit is more involved. After staging the changes I:

    $ git commit --fixup <sha>
    

    (The sha is typically picked through fuzzy finding as well).

    Then I rebase to actually apply the fixup those two commits:

    $ git rebase -i --autosquash <earlier-sha>
    

    The <earlier-sha> is also selected with a fuzzy finder, and must be a commit prior to the one being fixed.

    I think we can do better.

    git-fix

    I realized much of this could be automated. I prompted an agent to create a script for me that would do the following:

    # Amends the last commit with staged changes
    $ git fix
    # Amends the specific commit with staged changes
    $ git fix <sha>
    

    What about conflicts? Abort and leave the repo in the same state as it was before the command.

    I also instructed it to use TDD, which I believe effectively guided the agent.

    Specs:

    require "spec_helper"
    require "open3"
    require "tmpdir"
    require "fileutils"
    
    RSpec.describe "git-fix" do
      let(:script_path) { File.expand_path("../settings/.bin/git-fix", __dir__) }
    
      around do |example|
        Dir.mktmpdir do |dir|
          Dir.chdir(dir) do
            # Initialize a git repo
            system("git init --quiet")
            system("git config user.email 'test@example.com'")
            system("git config user.name 'Test User'")
            system("git config commit.gpgsign false")
    
            example.run
          end
        end
      end
    
      it "amends a commit with staged changes" do
        # Create initial commit
        File.write("file.txt", "line 1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial commit'")
    
        # Create a second commit that we'll want to fix
        File.write("file.txt", "line 1\nline 2\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add line 2'")
        target_commit = `git rev-parse HEAD`.strip
    
        # Now we want to fix the second commit (no commits after it)
        File.write("file.txt", "line 1\nline 2 fixed\n")
        system("git add file.txt")
    
        # Run git-fix
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be(true), "Command failed with output: #{output}"
    
        # Verify the commit was amended
        fixed_content = `git show HEAD:file.txt`
        expect(fixed_content).to eq("line 1\nline 2 fixed\n")
    
        # Verify commit count is still 2
        commit_count = `git rev-list --count HEAD`.strip.to_i
        expect(commit_count).to eq(2)
      end
    
      it "works with short commit references" do
        # Create initial commit
        File.write("file.txt", "initial\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial'")
    
        # Create target commit
        File.write("file.txt", "initial\nv1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add v1'")
        target_commit = `git rev-parse --short HEAD`.strip
    
        # Stage a fix
        File.write("file.txt", "initial\nv1 fixed\n")
        system("git add file.txt")
    
        # Run git-fix with short SHA
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be true
        expect(File.read("file.txt")).to eq("initial\nv1 fixed\n")
      end
    
      it "amends a middle commit when changes don't overlap" do
        # Create initial commit with two files
        File.write("file1.txt", "content 1\n")
        File.write("file2.txt", "content 2\n")
        system("git add .")
        system("git commit --quiet -m 'Initial commit'")
    
        # Modify file1 in second commit
        File.write("file1.txt", "content 1 modified\n")
        system("git add file1.txt")
        system("git commit --quiet -m 'Modify file1'")
        target_commit = `git rev-parse HEAD`.strip
    
        # Modify file2 in third commit (different file, no overlap)
        File.write("file2.txt", "content 2 modified\n")
        system("git add file2.txt")
        system("git commit --quiet -m 'Modify file2'")
    
        # Now fix the second commit (file1)
        File.write("file1.txt", "content 1 fixed\n")
        system("git add file1.txt")
    
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be(true), "Command failed with output: #{output}"
    
        # Verify both files have correct content
        expect(File.read("file1.txt")).to eq("content 1 fixed\n")
        expect(File.read("file2.txt")).to eq("content 2 modified\n")
    
        # Verify commit count is still 3
        commit_count = `git rev-list --count HEAD`.strip.to_i
        expect(commit_count).to eq(3)
      end
    
      it "defaults to HEAD when no commit is specified" do
        # Create initial commit
        File.write("file.txt", "line 1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial commit'")
    
        # Create second commit
        File.write("file.txt", "line 1\nline 2\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add line 2'")
    
        # Stage changes to fix the last commit (HEAD)
        File.write("file.txt", "line 1\nline 2 fixed\n")
        system("git add file.txt")
    
        # Run git-fix without specifying commit (should default to HEAD)
        output, status = Open3.capture2e(script_path)
    
        expect(status.success?).to be true
    
        # Verify the last commit was amended
        expect(File.read("file.txt")).to eq("line 1\nline 2 fixed\n")
    
        # Verify commit count is still 2
        commit_count = `git rev-list --count HEAD`.strip.to_i
        expect(commit_count).to eq(2)
      end
    
      it "shows error when nothing is staged" do
        File.write("file.txt", "content\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial'")
        target_commit = `git rev-parse HEAD`.strip
    
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be false
        expect(output).to match(/nothing.*staged/i)
      end
    
      it "aborts rebase and restores staged changes on conflict" do
        # Create initial commit
        File.write("file.txt", "line 1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial commit'")
    
        # Create second commit
        File.write("file.txt", "line 1\nline 2\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add line 2'")
        target_commit = `git rev-parse HEAD`.strip
    
        # Create third commit that modifies the same line
        File.write("file.txt", "line 1\nline 2 modified\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Modify line 2'")
    
        # Try to fix the second commit with a conflicting change
        File.write("file.txt", "line 1\nline 2 different fix\n")
        system("git add file.txt")
    
        # Run git-fix (this should fail due to conflict)
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be false
        expect(output).to match(/conflict/i)
        expect(output).to match(/aborted/i)
    
        # Verify we're not in the middle of a rebase
        expect(Dir.exist?(".git/rebase-merge")).to be false
        expect(Dir.exist?(".git/rebase-apply")).to be false
    
        # Verify the fixup commit was removed
        last_commit_msg = `git log -1 --pretty=%B`.strip
        expect(last_commit_msg).to eq("Modify line 2")
    
        # Verify the changes are still staged
        staged_diff = `git diff --cached --name-only`.strip
        expect(staged_diff).to eq("file.txt")
    
        # Verify the staged content is what we wanted to fix
        staged_content = `git diff --cached file.txt`
        expect(staged_content).to include("-line 2 modified")
        expect(staged_content).to include("+line 2 different fix")
      end
    end
    

    Code:

    #!/usr/bin/env bash
    # Amends the referenced commit with the code currently staged
    # Usage:
    #   git fix [commit-sha-reference]
    #
    # If no commit reference is provided, defaults to HEAD (the last commit).
    #
    # This script uses git's --fixup and --autosquash features to amend a commit
    # in your history. It works best when the changes don't overlap with subsequent
    # commits. If there are conflicts during the rebase, you'll need to resolve them
    # manually.
    
    set -euo pipefail
    
    # Default to HEAD if no commit reference is provided
    commit_ref="${1:-HEAD}"
    
    # Check if there are staged changes
    if git diff --cached --quiet; then
      echo "Error: Nothing is staged. Stage your changes with 'git add' first." >&2
      exit 1
    fi
    
    # Resolve the commit reference to a SHA before creating the fixup
    # This is important because creating the fixup will change where HEAD points
    target_commit=$(git rev-parse "$commit_ref")
    
    # Create a fixup commit
    git commit --fixup="$target_commit"
    
    # Get the parent of the target commit to use as rebase base
    rebase_base=$(git rev-parse "$target_commit^")
    
    # Rebase with autosquash (non-interactive)
    # If the rebase fails, we need to clean up
    if ! GIT_SEQUENCE_EDITOR=: git rebase --autosquash -i "$rebase_base" 2>&1; then
      echo "" >&2
      echo "Error: Rebase failed due to conflicts." >&2
      echo "Aborting rebase and restoring your staged changes..." >&2
    
      # Abort the rebase
      git rebase --abort
    
      # Uncommit the fixup commit but keep changes staged
      git reset --soft HEAD~1
    
      echo "Aborted. Your changes are still staged." >&2
      echo "Please resolve conflicts manually or modify your changes." >&2
      exit 1
    fi
    

    After only a few days of use, I can already see me using this regularly.

    Read on →

  • The REPL: Issue 134 - October 2025

    Vibing a Non-Trivial Ghostty Feature

    Mitchell Hashimoto’s extensive post details his use of AI to code a Ghostty1 feature, mirroring my own experience with AI. It excels at prototyping and boilerplate, and is good at explaining existing code. However, it requires constant supervision; I frequently tweak and guide its output. Sometimes, it misses the mark entirely and gets stuck.

    Abstraction, not syntax

    Alternative configuration formats solve superficial problems. Configuration languages solve the deeper problem: the need for abstraction.

    Abstraction, while simplifying expression, comes at the the cost of generating the configuration file, as the article explains. However, the author omits that YAML supports references, offering a degree of abstraction:

    # Define reusable base configurations
    .defaults: &defaults
      region: "eu-west"
    
    .lifecycle_policies:
      hourly: &hourly-policy
        delete_after_seconds: 345600  # 4 days
      daily: &daily-policy
        delete_after_seconds: 2592000  # 30 days
      monthly: &monthly-policy
        delete_after_seconds: 31536000  # 365 days
    
    buckets:
      - <<: *defaults
        name: "alpha-hourly"
        lifecycle_policy: *hourly-policy
      - <<: *defaults
        name: "alpha-daily"
        lifecycle_policy: *daily-policy
      - <<: *defaults
        name: "alpha-monthly"
        lifecycle_policy: *monthly-policy
      - <<: *defaults
        name: "bravo-hourly"
        lifecycle_policy: *hourly-policy
      - <<: *defaults
        name: "bravo-daily"
        lifecycle_policy: *daily-policy
      - <<: *defaults
        name: "bravo-monthly"
        lifecycle_policy: *monthly-policy
    

    While this is not a while loop, it does remove the repetition, and the need to check each of the values multiple times.

    Locating Elements in Hash Arrays Using Pattern Matching in Ruby

    Pattern matching in Ruby is still relatively new. I’ve only seen it used sparingly, but this use is concise. I’m still mulling over the syntax. Assigning to feels weird. A few years ago, when numbered parameters (e.g., collection.each { _1.do_something }) were introduced, I didn’t care much for them. Now, I am on a team that uses them constantly and the syntax has grown on me. Perhaps pattern matching like this will take hold in time.

    system = {
      users: [
        { username: 'alice', role: 'admin', email: 'alice@example.com' },
        { username: 'bob', role: 'user', email: 'bob@example.com' },
        { username: 'charlie', role: 'moderator', email: 'charlie@example.com' }
      ]
    }
    
    system => {users: [*, { role: 'moderator', email: }, *]}
    puts email # charlie@example.com
    
    1. Ghostty is an excellent terminal emulator. After using iTerm 2 for years, I tried ghostty and haven’t looked back. 

    Read on →

  • The REPL: Issue 133 - September 2025

    Yet another LLM rant

    Dennis Schubert discussed LLMs – like half the internet.

    LLMs can be a useful tool, maybe. But don’t anthropomorphize them. They don’t know anything, they don’t think, they don’t learn, they don’t deduct.

    This is true. Although they can fake it quite a bit, especially agents that break down the next steps, read files, and come up with a list of todos, etc.

    I find myself thinking that AI is both unreasonably hyped-up and incredibly useful. I’ve been able to get proof-of-concept projects working in minutes with only superficial knowledge of the underlying technologies. Denying that agentic AI is powerful seems foolish. Also, thinking that they can replace engineers wholesale is equally foolish.

    We are in for a time of disruption.

    Pick the wrong tool for the job

    Sometimes the wrong tool for the job is right for you at a given moment in time. In this case, the author wanted to use Ruby to be able to iterate faster. Being able to learn what the users really want was more important than having the perfect technical solution

    Read on →