Blog

Going Back in Time

I’ve been trying to adapt my coding workflow to LLMs for a while. The way I use LLMs has definitely changed over time and I’ve written about it here and here.

In my experience, around late 2025/early 2026, SOTA models crossed some threshold and made a significant leap in coding performance. When Opus 4.5 and then later GPT-5.3-Codex came out, suddenly you could trust the output of LLMs a lot more. Sure they still made some mistakes (and still do) but these were significantly rarer than before. You could expect the code to be right, whereas I felt like before then my expectation was that we’d have to iterate quite a bit.

Over the months following that big leap, I started shipping so much more, especially my own stuff. It’s incredibly easy to build whatever you want now with LLMs. At work, the speedup was a little less dramatic since you have to be careful with production software that’s actually being used by a ton of people, and then there are reviews, etc. But still, LLMs became a huge part of my workflow. I rarely wrote code by hand anymore.

Until, that is, a few weeks ago when suddenly our access to modern LLMs was cut at work. We were told that we could no longer use anything except an in-house harness that routes us to a pool of older models. The most capable of the models is Sonnet 4.5. We can’t select the models: the harness decides which model to route requests to, so it’s hard to tell how often we actually even get to use Sonnet.

Needless to say, this was a bit of a shock and many an argument was had. Regardless, it is what it is and we’ve had to adapt. And this is where that big leap comes into play. Turns out we got used to workflows that are no longer viable. I can no longer have an LLM write a feature. It just can’t.

These LLMs now like to rewrite things like this:

for _, f := foos {
  if bar(f) {
    return baz(ctx, f)
  }
}

to

for i := range foos {
  f := foos[i]
  if bar(f) {
    result := baz(ctx, f)
    return result
  }
}

I can guess why it’s changing the for variable: it’s trying to protect against the old Go gotcha in which the reference of the loop variable was kept after the end of the iteration, but this hasn’t been the case for years and certainly isn’t an issue on the Go version we require in the codebase. Why the result variable introduction is beyond me. None of these rewrites were needed or requested, the harness or the LLM simply thought this has to change.

Even things like code reviews are more challenging. I got used to having LLMs assist in reviewing PRs, but now with these models these reviews are much less useful. I realized I had to stop relying on them after the available models went 0 for 7: 0 actual flaws in 7 findings.

The example that brought painful memories of the past was when one of the models found a critical issue in a code snippet like this (not the actual code):

if req.Foo == nil {
  return nil, status.Error(codes.InvalidArgument, "missing foo")
}

bar.Baz = req.Foo.Baz

It reported a critical issue: if req.Foo was nil, then referencing req.Foo.Baz would cause a nil-pointer dereference panic. Obviously that is not true here because if req.Foo was nil, the function would have returned before the dereference. I pointed it out to the LLM which happily responded “You’re absolutely right…”

I rarely see a modern LLMs miss a validation/initialization right above a use. The other false positives were mostly like this: easy to disprove at a glance. But others would be less obvious to someone not familiar with the system. The claims were clearly wrong to me, but proving them wrong involved some investigation, which I had to do just in case I was wrong.

All of this takes time. Individually, these false positives don’t look like much, but they all add up. And then you have people on the team trusting the LLM reviews and posting their findings in your PRs, and then you need to spend time proving that no, that’s not right. It’s a lot of wasted time for something that’s supposed to accelerate you.

So, while for my personal projects LLMs feel like magic, at work I’m back in time by maybe a year or so. I can no longer trust the output of my LLM. It stopped accelerating me and started slowing me down. I can only safely use LLMs for small and very specific changes that I have to overexplain, review line by line, and iterate.

It’s hard to see how much things have evolved in a year until you have to go back in time.

谢谢你,中国! (Thank you, China!)

First things first: Happy Canada Day, everyone! Bonne fête du Canada, tout le monde !

Yes, I know. Starting a post that celebrates Canada Day with a giant title thanking China might feel a bit ironic, if not entirely contradictory. But stick with me for a minute. Because if you are a Canadian working in tech right now, navigating a world where our longest-standing friendship and alliance has ruptured into open hostility from the US government, you should be thanking them too.

For decades, the Canadian tech ecosystem operated under an unspoken assumption: we were fundamentally part of the same digital backyard as Silicon Valley. We relied on the same infrastructure, trusted the same partners, and assumed that a shared border meant shared access to the future of technology.

The rise of large language model regulation in the US has shattered that illusion. As LLMs became the foundational infrastructure for the next generation of software, the narrative shifted toward a suffocating, centralized reality. Suddenly, it looked like access to “intelligence” would be controlled by a couple of trillion-dollar American tech giants, operating under the tight regulatory grip and arbitrary “national security mandates” of an increasingly authoritarian and hostile US government. That’s… not great.

The message to us and to the rest of the world was clear: if you want to build next-gen software, you have to rent it from them and you have to behave or they’ll turn off the tap.

When the US government forced Anthropic to turn Fable and Mythos off a couple of weeks ago, it hit me how all of a sudden things could turn. And that’s when I starting thinking about how different things could have been had China not chosen the path of opensource.

The Open-Source Weapon

Had Chinese AI labs followed the American playbook, the trap would have snapped shut.

When the US president put measures to prevent chips from being sold to China, they inadvertently scored one of the biggest own goals ever, because faced with severe hardware restrictions and intense domestic competition, Chinese companies and research labs like Alibaba, Z.AI and Moonshot took a radically different path. While American companies felt safe in closing their gates, shifting from open research to aggressive, closed-source monopolies, the Chinese chose to weaponize open weights.

They realized the fastest way to bypass the American monopoly was to simply give the models away.

And they didn’t just dump mid-tier toys. Their models are improving at a much higher pace compared to the American ones and have pretty much caught up in practical terms. They’ve consistently commoditized the technology that the US would love to leverage against us.

Autonomy through Proliferation

There is a profound irony here. China has long been known for its heavy domestic internet censorship and state oversight. Yet, they’ve inadvertently become the world’s greatest guarantor of decentralized, open-source AI infra.

By flooding the ecosystem with high-quality opensource models, they’ve made it functionally impossible for an authoritarian US government to lock down the future of software development.

Once a model like GLM 5.2 or Kimi K2.7 are on Hugging Face, downloaded, and running locally on hardware right here in Canada, the gates cannot be closed. It belongs to the global developer community. Better yet, it can be fine-tuned, quantized, and run entirely offline.

This isn’t about blind defence of the Chinese government or anything. It’s a pragmatic look at power dynamics: hegemony is the absolute enemy of innovation, and proliferation is the ultimate antidote to bullying.

When global superpowers compete by releasing open infrastructure, developers win. In fact, the aggressive opensourcing of Chinese frontier models may threaten the duopoly of Anthropic and OpenAI. OpenAI has already postponed their IPO, as companies start having trouble reconciling the costs of inference charged by these labs with whatever gains in productive they’re seeing from them.

Because Chinese labs chose to open up their models, the dream of an API-locked AI dystopia where Canada and other “middle powers” can be frozen out at the border is dead. We have choices, which means we have autonomy.

So, to the engineers and teams pushing the boundaries of open weights on the other side of the world, from a developer celebrating a complicated Canada Day: 谢谢你,中国。 You kept the future of AI open when our neighbours tried to lock it away.

Updated LLM Coding Workflow

Back in January I posted about how I view LLMs, which included my workflow of doing LLM-assisted coding. To summarize, my workflow was:

  1. Reverse rubber-ducking
  2. Planning and writing a spec file
  3. Implement each phase of the plan, one by one
  4. Validation and commit

I can say lots have changed since January. For one, models are significantly better and more reliable. As well, I feel like I’ve got better at steering them. If I look at the above list by itself, without details, it doesn’t feel like things changed so much, but look closer and it’s a whole new world. My current workflow is an evolution of the above.

Before continuing, let me make something clear: this is my professional workflow. It’s what I use to write production code on cloud services.

Phase 0: Reverse Rubber-ducking

I still have this, but I no longer use a chat interface. I start directly with an agent (almost always OpenCode) and I now first get the agent to engage with the code before anything else. Let’s say I need to make a change to the flux capacitors, so I go and tell the agent what I think happens:

This is cloud-service-foo and it handles requests to create farbelizer connectors. I believe it then nimbolizes the farbelizers before sending them to cloud-service-bar that processes them through the flux capacitors. Check what the actual flow is and summarize it for me.

Most of the times – though not always – I know exactly how the flow works, but I do this to sort of prime the LLM for discussing what I want to change. I just found that it tends to work well for me; better then just telling it directly what I want to change.

An indirect effect of doing this is that sometimes it will tell me something that doesn’t meet my understanding, so I ask details to figure out if it’s really something I missed or just something the LLM got wrong.

When I know the LLM has the context, I will say something like

I have an issue where if the farbelizer connector starts with “foo”, then the flux capacitors should suppress the harmonic back-feeding before it reaches the primary gimbal housing.

I often add a little more about what the actual goal is, but it’s something like this. This usually causes the LLM to tell me what it thinks should be done. It will sometimes ask a question or two, but eventually it will give me a solution. Many times the solution is one I know I don’t want due to some constraint and I will tell it. Sometimes I steer it a bit more and tell it what I think we should do.

And then when I’m happy, I’ll say:

Plan a series of independent PRs to implement this. List which ones can be done in parallel vs linearly

That’s it. That’s the entire plan phase now. I no longer need a spec file.

Phase 1: Implementation

Given the list of PRs, I will ask it to implement them either a few in parallel or, if there’s a dependency, one by one. I also now let the LLM agent commit its changes. (if they were in parallel, I also let it push the changes.)

Again, that’s it?

Phase 2: Validation

I then test the work locally, I still insist on doing that because I don’t want to cause a SEV. I’ll then push the branches and review the diffs myself in GitHub before asking for others to review: I want to avoid wasting people’s time.

You still have to watch them

I had an interesting interaction with GPT 5.5 a few weeks ago, where it wrote code that was akin to this:

attempt := 0

for {
	attempt++
	if attempt > maxAttempts {
		return errTooManyAttemps
	}
	err := fetchData(ctx)
	if err != nil {
		switch {
		case errors.Is(err, errTimeout):
			fmt.Println("Warning: Timeout occurred. Retrying...")

		case errors.Is(err, context.Canceled):
			fmt.Println(" -> Context was canceled. Exiting...")

		default:
			return err
		}
		
		time.Sleep(100 * time.Millisecond) 
	}
}

When I saw that, I immediately knew it didn’t look right, so I asked the agent about the case with the context.Canceled and it happily explained to me that it would log the error and then return with default:. I said, no, it won’t, that’s not how Go switches work. And it insisted! “I understand your confusion, but because there is no break statement, the code will simply fall through the next case.”

No, it forking won’t! So I told it to prove it by writing a test that returned a context canceled. It did, caught the infinite loop and conceded.

My point? They can still make mistakes. I have to check them.

Conclusion

That said, I will concede that the LLMs are so much better now and that these errors are getting more and more rare. My flow is much quicker than before. I still review code like a caveman, I still make sure the LLM gets what I want it to do. But I basically killed the entire “plan” step. It’s just not needed. And I almost never write code by hand.

Your App Subscription Is Now My Weekend Project

I pay for a lot of small apps. One of them was Wispr Flow for dictation. That’s $14 CAD/month that I was paying until I had a few lazy days visiting my mother. And then on the afternoon of New Year’s Day, I vibecoded Jabber.

Now, don’t get me wrong, Jabber is not “production quality.” I would never sell it as a product or even recommend it to other people, but it does what I needed from Wispr Flow, and it does exactly the way I want it to. For free.

At work, I’m often asked to make small videos showing some support agent how something works, or sharing some knowledge with new team members, or just a regular demo of something. In the past, I used to use Loom, which costs $15/month. So after creating Jabber, I got excited and vibecoded Reel.

Reel does exactly what I wanted Loom to do: I can record my camera, I can move it around, and I get to trim the video after it’s done (I don’t remember being able to do that with Loom).

Then just yesterday, a friend of mine was telling me how he got tired of paying for Typora and decided to vibecode his own Markdown editor. And that gave me the idea of creating an editor for my blog.

That’s Hugora! Yes, horrible name, but who cares? It’s just for me. I get to edit my Hugo blog just the way I like. It even shows my site theme.

You see the pattern here?

All of these $10/month apps are suddenly a weekend project for me. I’m an engineer, but I have never written a single macOS application. I’ve never even read Swift code in my life, and yet, I now can get an app up and running in a couple of hours. This is crazy.

Last year, a Medium post predicted:

Most standalone apps will be “features, not products” in the long run — easy to copy and bundle into larger offerings.

And I think we’re there. I don’t know what that means for the future of our industry, but it does seem like a big shift.

I’m still skeptical of vibecoding in general. As I mentioned above, I would not trust my vibecoding enough to make these into products. If something goes wrong, I don’t know how to fix it. Maybe my LLM friends can, but I don’t know. But vibecoding is 100% viable for personal stuff like this: we now have apps on demand.

Being Specific when Pairing with Bots

A couple of days ago I posted about my workflow and I made light fun of something I do:

I also give it context to save time. The agents nowadays are very smart and can find their way, but I can shortcut that by giving it hints “in internal/foo/foo.go there’s a function called DoFoo() and it does this and that and I want it to do that other thing before that” or whatever. Less tokens, faster iteration. This is probably astrology for nerds, pure superstition at this point, but I still do it.

Turns out, maybe it’s not really astrology for nerds? Today, Quinn Slack shared an article about How To Pair with an Agent and in it, the author says “The more you can specify, the better” and gives this example of a good, specified prompt:

Specified prompt:

Build a new API endpoint for user notifications. Follow the pattern in src/api/messages.ts as your reference. Run the API tests after each step. Don’t move on until they pass.

You gave it a reference to follow and a way to check its own work. Now you can step away. Let the agent iterate until the tests pass.

So maybe it’s not astrology for nerds, but a good practice? I always felt like it gave me better results, but I wasn’t 100% sure if it wasn’t just a superstition. Nice to see it’s probably not.