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
SHA1feb1740a0e11f5d89835fd5f926fe83e66b00ed
MessagePublish to the registry over HTTPS, and stop claiming permissions grants access (#175) Vikunja task 234. The first real publish failed with "http: server gave HTTP response to HTTPS client" against 192.168.1.3:3000 — the job derived its registry host from GITHUB_SERVER_URL, which resolves to the runner's internal, plain-HTTP address. Rob chose to use the public HTTPS endpoint rather than opt out of TLS on the LAN, and that turned out to need no workaround at all: git.robware.uk/v2/ answers with a genuine OCI 401, so docker login, tag and push work unchanged. REGISTRY_HOST is now a single job-level literal read identically by the login step, the tag-existence guard, the push loop and the summary, with a new step confirming the registry answers over HTTPS before login so a transport failure is distinguishable from an authorisation one. Design record: ADR-0149, which also records why skopeo, crane and a buildx insecure-registry builder all became moot once HTTPS was confirmed. A second defect was corrected in the same change. Forgejo ignores the permissions job key entirely, so ADR-0148's claim that permissions: packages: write was what granted the job registry access was false. The dead block is removed and the real mechanism — an Authorized Integration Rob configured for this repository — is recorded in ADR-0150, which partially supersedes that section rather than editing frozen text. The evidence for that correction was itself corrected during review. The first version cited a build-log warning, and review pulled the raw job logs for every run where permissions was present and found no such line anywhere. The warning is real — Rob reported it from Forgejo's pipeline view — but it does not appear in the logs the API exposes. So the record now rests on Forgejo's own documentation, cites Rob's observation as corroboration, and states the API-log absence as a fact in its own right. That gap is worth knowing: it is the second time this instance has been found to surface in its UI what its API will not return. The publish path itself remains unproven, and says so plainly: pull_request runs never reach it by design. The push run for this merge is what will settle it. Merged with an admin override: branch protection requires one approving review, which Forgejo will not accept on a self-authored pull request.
Timestamp06:52:23 on Saturday the 15th 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