Robware Software by Rob

Welcome

Hello, I'm Rob. I'm a senior software engineer at On The Beach. Professionally my focus has been in backend development primarily using C#. In my spare time I spend my time riding bikes or making stuff, typically involving an Arduino.

This website is primarily an outlet for me to write about things which have been technically challenging, either in a professional or personal capacity, though not limited to that.

If you wish to get in contact, then get in touch via my LinkedIn profile.

Latest code commit

RepositoryPlaceMark
Branchmain
SHA89ec4953bc6192654b463a1c1117f5a8d16132ab
MessageLet a user unlink an external provider (#95) ADR-0017 links an external identity by explicit action from an authenticated session; unlinking was the same shape and was impossible, so once linked a provider stayed linked forever. This builds the app's first WebUI step-up surface to gate it, which is why task 161 deferred it rather than bundling it. The lockout invariant is the point of the ticket: an account with no password and no remaining link would be unreachable, so the last usable credential cannot be removed. The settings page says what to do first — set a password, or link another provider — rather than refusing into a dead end. The concurrency guard took two rounds and the disagreement was the useful part. The first test used Task.WhenAll over two calls, and author and reviewer got opposite results from the same mutation on the same machine: red five times out of five running the single test, green three times out of three running the whole class. Neither was wrong. Task.WhenAll starts two tasks; it does not make them overlap, and when the first commits before the second reads, the second refuses correctly for the wrong reason — indistinguishable from the guard working. It is now deterministic. A gate connection holds the same FOR UPDATE the repository takes, a separate monitor connection polls pg_stat_activity until two backends are actively blocked on that statement, and only then does the gate commit. Removing the lock now fails on the contention assertion itself, in both run shapes, rather than on a downstream count that could pass by luck. The child-row lock on external_identities was dropped, having been proved redundant rather than merely unneeded — every caller already serialises on the parent users row. A lock that guards nothing a reader could rely on is worse than no lock, because it reads as a guarantee. Reviewed by the coordinating agent, this session having reached its subagent limit, which the review comments record. Merged with an admin override: branch protection requires one approving review, which Forgejo will not accept on a self-authored pull request.
Timestamp19:25:48 on Thursday the 6th of August 2026

Latest Blog Post

Expanding local LLM capabilities by creating skills from documentation

I've been using OpenCode lately with locally running LLMs with LM Studio, mostly with the Qwen 3.6 35B A3B model. Compared to the likes of Claude, these local models have some significant limitations. One major limitation I was finding with Qwen is that it would always generate FluentAssertions syntax for unit tests despite having Shouldly specified. It seems to understand I want to use Shoudly, but would get stuck generating FluentAssertions syntax. Quite annoying. In order to get around this I decided I'd create a custom skill from the documentation, and then get my agent to explicitly load it.

The steps are pretty basic:

  1. Grab a copy of the git repo
  2. Navigate to the documentation folder
  3. Using OpenCode's own Build agent, tell your LLM to create a skill from the documentation

It really is that simple. I did the same thing with NSubstitute.

~/.config/opencode/skills/shouldly$ tree
.
├── references
│   ├── completeIn.md
│   ├── configuration.md
│   ├── dictionary
│   │   ├── containKeyAndValue.md
│   │   ├── containKey.md
│   │   └── README.md
│   ├── dynamicShould.md
│   ├── enumerable
│   │   ├── allBe.md
│   │   ├── contain.md
│   │   ├── empty.md
│   │   ├── have.md
│   │   ├── oneOf.md
│   │   ├── README.md
│   │   ├── shouldBe.md
│   │   ├── subsetOf.md
│   │   └── unique.md
│   ├── equality
│   │   ├── assignableTo.md
│   │   ├── exampleClasses.md
│   │   ├── greaterLessThan.md
│   │   ├── haveFlag.md
│   │   ├── inRange.md
│   │   ├── matchApproved.md
│   │   ├── notBe.md
│   │   ├── null.md
│   │   ├── ofType.md
│   │   ├── oneOf.md
│   │   ├── README.md
│   │   ├── sameAs.md
│   │   ├── shouldBe.md
│   │   ├── ShouldMatchApprovedChanged.png
│   │   ├── ShouldMatchApprovedInitial.png
│   │   └── trueFalse.md
│   ├── exceptions
│   │   ├── notThrow.md
│   │   ├── README.md
│   │   └── throw.md
│   ├── getting-started.md
│   ├── satisfyAllConditions.md
│   ├── string
│   │   ├── contain.md
│   │   ├── endWith.md
│   │   ├── match.md
│   │   ├── null.md
│   │   ├── README.md
│   │   ├── shouldBe.md
│   │   └── startWith.md
│   └── upgrade
│       └── 3to4.md
└── SKILL.md

I also recommend creating an agent to specialise in software development and explicitly requesting the skill(s) in the agent file's frontmatter:

skills: csharp-developer, shouldly, nsubstitute

I also got it to create documentation on the Seeed Studio XIAO ESP32C3 by telling it to turn the getting started page in to a skill, and also the CAN Bus expansion board, to help with my campervan control system project.

Doing this has significantly improved the accuracy of the output and reduced the number of times it gets stuck in a loop and also reduced the amount of time spent thinking.

Posted on Friday the 10th of July 2026

View more