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
SHA426a7e9175f1cfdc52f6a95f1f25647fc0983d7f
MessageMigrate the remaining inline confirmations to ConfirmationModal (#213) Task 221. One visual language for "are you sure?" rather than two — presentation consolidation, not a redesign, with every site keeping its own wording, destructive styling, disabled-while-in-flight states, focus behaviour and Escape semantics. PlaceFormPanel's delete, GroupFormPanel's delete and PendingInvitationRow's decline all move to the shared ConfirmationModal. GroupFormPanel's cascading place-count wording is preserved deliberately: group deletion cascades, so that text is load-bearing rather than decoration. ConfirmationModal grew a Busy parameter, disabling Confirm and Cancel while a mutation is in flight. ADR-0136's own Alternatives anticipated and deferred exactly that, which review confirmed by reading the record rather than taking it on trust, so the shared component grew instead of a second variant being forked and no new ADR was needed. The decline site was structurally the hardest and not a markup swap. Nesting a confirmation inside a row that itself goes inert would have inertified the confirmation — precisely the bug ADR-0136's SuppressedByStackedOverlay episode already found in a real browser while every bUnit assertion passed. The confirmation and its state moved up to Account, which is now also the first ConfirmationModal stacked over ordinary page content rather than another ModalOverlay-hosted panel, so Account's own page content carries a new inert binding. Four sites are excluded, each with its reason recorded in code rather than left unremarked. PlaceForm's move confirmation stays a real form submit, because a button in a separate stacked modal sits outside the form and reopens the second-entry-point problem ADR-0066 forbids — the exception the ticket itself predicted. GroupMemberRow stays inline because ADR-0065 chose that deliberately for the list-scoped case and ADR-0136 never named it among this ticket's sites; converting it would have silently reopened a decision nobody revisited. GroupMembersPanel has no confirm markup of its own. And Account deletion has no UI at all — only an API client method — so the ticket's premise that it was a site to migrate was simply wrong. Each migrated site was watched to fail on its own, as the ticket required rather than as a representative sample: wiring the destructive action to fire without confirmation reddened PlaceFormPanel's permanence-naming test on an unexpected DELETE, GroupFormPanel's place-count test the same way, and Account's cancel test by making the confirmation vanish before Cancel could be clicked. DeclineInvitationJourneyTests is added as the one genuinely new browser-only path, since Account is the first non-panel ConfirmationModal host and bUnit is blind to inert. It caught a real locator bug on its first run. GroupFormPanel's delete still has no E2E coverage — a pre-existing gap, named rather than deepened, and worth a follow-up given stacking bugs here are invisible below a real browser. Review found no defects, checking behaviour preservation, the Account inert stacking, Busy, and all four exclusions against the source and the cited ADRs. It could not re-derive a per-site mutation itself — the sandbox blocks file edits in the review worktree — and said so plainly rather than implying otherwise. Run #892 at d755c66: build, e2e and container-images all green, tied to the SHA through the run's own logs, with the E2E suite at 63/63 in 3m14s. Coverage held from a Release build with the merged Cobertura report, WebUI reading 91.7% against its 91.6% baseline. Merged with an admin override: branch protection requires one approving review, which Forgejo will not accept on a self-authored pull request.
Timestamp15:30:07 on Friday the 21st 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