Linked Data, Knowledge Graphs, Semantic Web and Art
Commissions
Commissions
Commissions can be requested on many topics or objects from still life, pet portraits, people in a scene and landscapes drawn using colour pencils.
Pricing
Prices include shipping in the UK only. Additional costs will be incurred for shipping to destinations outside the UK.
Portraits and Still Life
Paper size
Price
12″x9″
£200
12″x18″
£400
Landscapes and other
PaPer size
Price
12″x9″
£300
12″x18″
£600
How it works
Send an enquiry below with your details and request. I will be in contact for you to provide a few good quality digital photos. We will then discuss which one you prefer. Where the photos are of a poorer quality and good quality photos are not available, please note that the art work will contain less detail. We will discuss this as well when we look at the photos together. On agreement of the work to be done and the expected timeline for completion, a 25% booking fee is required to secure your commission. On completion of the work, the art will shared digitally and on full payment it will be delivered to your preferred address.
As many developers, I am running several AI coding agents (Claude Code sessions in my case) in parallel on the same repository, each in its own git worktree. Git handles the code isolation beautifully: every worktree has its own files, its own branch, its own uncommitted changes. The breakdown comes at test time. My project’s end-to-end tests spin up a complete stack — a Solr index, a Spring Boot backend, a React frontend and a Nextflow data-load pipeline — and the moment two worktrees tried to test at the same time, they collided over ports, data directories and indexes. One agent’s test run wiped the index another agent was querying.
For those of you who have not solved this problem yet, I in this post give a step-by-step guide to fixing that: making every worktree able to run a complete, isolated end-to-end test stack on demand. The worked example is OxO2, the SSSOM-compliant ontology mapping service I work on at EMBL-EBI, but the recipe transfers to any repo — my Solr could be your Postgres, Neo4j or Redis.
The problem: git isolates code, not runtime
git worktree gives each checkout its own files. Ports, data directories, PID files and log directories, however, live on the machine, shared by everything running on it. Two Solr instances started with default settings both want port 8983, the same home directory and the same index files. This is not misuse — it is the designed behaviour. Git solves the code half; the runtime half is yours to solve.
The fix rests on one idea:
Derive every port and every state directory from a single per-worktree number.
Give the main checkout index 0 and each worktree the next free integer. Everything else — ports, data directories, config choice, service URLs — is computed from that index by one shared script. No registry, no negotiation, no remembering. Any terminal, and crucially any AI agent session, can compute the whole environment from the index.
The rest of this post builds that up in eight steps.
Step 1 — Inventory what your stack shares
Before writing any scripts, list every machine-global resource your stack and your tests touch. For OxO2 the list looks like this:
Resource
Default
Collision symptom
Frontend dev server port
8080
“address already in use”
Backend port
8081
“address already in use”
Solr port
8983
second instance fails to start
Solr stop port
7983 (port − 1000)
mysterious failures with “different” ports
Test Solr port
8984
e2e suites collide
Data directory
$OXO2_DATA
one run overwrites another’s pipeline output
Solr home (cores + index)
$SOLR_HOME
parallel end-to-end tests corrupts index
Test data / test Solr home
*_TEST dirs
integration tests trample each other
Pipeline (Nextflow) work dir
$NEXTFLOW_DIR
interleaved intermediate files
Solr logs directory
install default
interleaved, undebuggable logs
Two entries deserve emphasis, because they are the ones that have bitten me:
A service can claim more ports than you configured. Every Solr instance also binds a stop port at SOLR_PORT − 1000 and, with JMX enabled, SOLR_PORT + 10000. Two Solrs on “different” ports 8983 and 7983 still collide: the first’s stop port is the second’s listen port.
A service’s state is several directories, not one. For Solr that is its home (cores, configs, index), its PID directory (which bin/solr status and stop consult) and its logs directory. Share any one of them between two instances and they are entangled.
Step 2 — Make every resource configurable through the environment
Every item on the inventory must be settable per checkout, with the old value as the default so existing setups keep working. In practice this means touching each service once, in whichever way that service supports:
Native environment variable. Solr’s bin/solr reads SOLR_PORT and SOLR_HOME natively — nothing to change.
Build-tool config reads the environment. OxO2’s vite.config.ts gained one line: port: Number(process.env.OXO_FRONTEND_PORT) || 8080,
Launcher script forwards the variable. OxO2’s startBackend.sh forwards OXO2_BACKEND_PORT into Spring Boot as -Dserver.port=…, only when the variable is set, so the default stays untouched.
Pipelines key off a URL. The data-load pipeline reads SOLR_URL and derives the port to start/stop its managed Solr from it, so pointing the URL at the right port is enough.
This step is the actual work of the whole exercise, and it is also where an AI agent can help you: ask it to find every hard-coded port and path in the repo and thread an environment variable through each one.
Step 3 — Choose a directory layout
I keep the main checkout, the worktrees and their runtime state as siblings under one project root, with the state outside the worktrees:
oxo2/ # project root (not itself a git repo)
├── oxo2/ # main checkout — index 0
├── worktrees/
│ ├── fix-ranking/ # worktree — claims index 1
│ └── cleanup-facets/ # worktree — claims index 2
├── worktree-state/
│ ├── fix-ranking/ # that worktree's private runtime state
│ │ ├── data/ test-data/
│ │ ├── solr-data/ test-solr-data/
│ │ ├── nextflow/ solr-logs/
│ └── cleanup-facets/ …
├── data/ solr-data/ … # full-corpus state, main checkout only
└── oxo2-env.sh # the one shared environment script
Why keep state outside the worktree? Index and pipeline state can be large, it slows git’s untracked-file scanning, and — most importantly — git worktree remove refuses a dirty tree. Keeping state in a sibling directory keyed by the worktree’s name makes removing the code and removing the state two separate, deliberate acts.
Note the asymmetry: the full production corpus (for OxO2: 134 GB of data plus a 38 GB Solr index) exists once, in the main checkout’s shared directories. Worktree loads are test-scale by design — that is Step 6.
Step 4 — One environment script, one number
Now the heart of it: a single script at the project root, outside the repo, that every checkout sources. It does four things — identify the checkout, claim an index, derive ports, derive state directories. Here is the skeleton, condensed from OxO2’s oxo2-env.sh:
#!/bin/bash
PROJECT_ROOT=/home/you/projects/myproject
MAIN_CHECKOUT=$PROJECT_ROOT/myproject
# --- 1. Identify the checkout this shell is in ---------------------------
# Only a checkout of *this* repo counts: its git-common-dir resolves to the
# main checkout's .git. Sourced anywhere else, fall back to main defaults.
# --- 5. Service URLs derive from the ports -------------------------------
exportSOLR_URL=http://localhost:$SOLR_PORT/solr
exportBACKEND_URL=http://localhost:$BACKEND_PORT
Two design decisions in the port scheme are load-bearing, and both come from traps I described in Step 1 plus one more:
Base 20000, spacing ×100. The spacing keeps each instance’s whole trio of ports (listen, stop at −1000, JMX at +10000) clear of every other instance’s trio. The base keeps everything the scheme can produce (roughly 19xxx–30xxx) above the privileged range (0–1023) and below 32768 — because Linux hands out ports 32768–60999 to outgoing connections (the ephemeral range), and a fixed dev port in that range works for weeks and then fails with “address already in use” when some outgoing connection happened to borrow it first. Advice you will find online to base your scheme at 40000 is subtly wrong for exactly this reason.
Index 0 keeps the legacy ports. My main OxO2 behaves exactly as it did before any of this existed, so nothing about my primary development flow changes.
The index-claiming logic deserves a note too: because the first source from a new worktree claims the lowest free index automatically and persists it in a gitignored .worktree-index file, there is no manual bookkeeping — and no way for a freshly copied environment file to silently reuse the donor worktree’s ports, which is the classic failure of copy-the-.env-around schemes.
With direnv hooked into your shell, that file loads when you cd into the worktree and unloads when you leave. Two properties make direnv exactly right for this job:
Every session agrees. Any terminal — and any AI agent session started in that worktree — gets the same ports and paths without remembering to source anything.
The direnv allow gate. direnv refuses to run a new or changed .envrc until you approve it. A fresh worktree’s environment therefore cannot load “accidentally”; approving it is the natural moment at which the worktree claims its index.
Add .envrc and .worktree-index to .gitignore. If you use Claude Code’s claude --worktree, you can list .envrc in .worktreeinclude so new worktrees arrive with the file already in place — since ours is one identical line everywhere, it can be copied verbatim safely.
Step 6 — Give worktrees a test-scale dataset that travels
An isolated stack is useless if loading the data takes hours and hundreds of gigabytes. The full OxO2 corpus — the whole of OLS plus every Mapping Commons registry — loads once, into the main checkout’s shared directories. Worktrees default to a different, committed config that loads a deliberately small dataset.
Getting that config to travel took one real change. Our first test config referenced fixture files by absolute path (file:///home/<user>/…) — machine-specific paths outside the repo, so the config worked in exactly one place. The fix was to commit trimmed fixture slices into the repo and get the data-load’s downloader to resolve a repo-relative path in the config against the config file’s own directory. Now every worktree — and CI — resolves the same fixtures.
The trimming is worth copying: each fixture keeps semantically important data. A small script regenerates the data from a full export whenever they need refreshing.
The principle I wanted to apply here is: a worktree dataload should be small enough to run quickly and real enough to catch real bugs, and its fixtures must live in the repo so every checkout resolves them identically.
Step 7 — Keep the test stack separate from the dev stack
There is a second collision axis I have encountered: within OxO2 we have integration tests that runs against Solr. These tests are somewhere between unit tests and end-to-end tests. Unlike end-to-end tests they test specific aspects of the dataload. OxO2’s integration tests therefore run against their own environment contract — OXO2_DATA_TEST, SOLR_HOME_TEST, OXO2_SOLR_HOST_TEST — never against the Solr used for end-to-end testing. The harness:
fails fast if any *_TEST variable is missing or blank, rather than falling back to the production values (a fallback here would be a data-destroying bug);
parses the test Solr port out of OXO2_SOLR_HOST_TEST and starts/stops an isolated Solr on it, once per suite;
injects the *_TEST values into the pipeline subprocess as the plain variables the pipeline expects, so the production pipeline code runs unmodified against the test workspace.
Notice how the two schemes compose: the env script derives bothSOLR_PORT and SOLR_PORT_TEST from the worktree index. Worktree 1 end-to-end tests run on 20183 while its integration tests runs on 20184; worktree 2 uses 20283 and 20284. Every combination of (worktree, dev-vs-test) has its own port and its own state directory, so n agents can each run the full end-to-end suite simultaneously, while their dev stacks stay up.
Step 8 — The daily workflow
With all the pieces in place, here is the entire ritual for a new parallel workstream:
# build, then run the integration tests — fully isolated
mvn clean install -DskipTests
mvn -pl myproject-integration-tests -am verify
# or bring up the dev stack for interactive work
./loadData.nextflow && ./startBackend.sh &
cd frontend && npm install && npm run dev
Then hand the worktree to an agent — every command it runs inherits the right ports and paths from direnv. Teardown is simply:
$SOLR_SCRIPT/solr stop# solr determines $STOP_PORT from $SOLR_PORT-1000
# From oxo2 main
git worktree remove ../worktrees/fix-ranking
rm-rf ../worktree-state/fix-ranking # state removal is deliberate
git worktree prune # for cases when stale worktrees are lingering
When something collides …
Diagnostic help for the three failures you will still occasionally hit:
“Address already in use” — ss -ltnp lists listening ports with owning processes; lsof -i :20183 names the culprit for one port. Also check the whole block (20100–20199) with ss -ltnp '( sport >= :20100 and sport <= :20199 )'.
$SOLR_SCRIPT/solr status shows the wrong instance — status and stop find instances via PID files in SOLR_PID_DIR, which defaults to the install’sbin/ directory, shared by every instance started from that install. Per-worktree PID (and logs) directories fix this.
Stale worktree metadata — after deleting a worktree directory manually, git worktree list still shows it; git worktree prune cleans up.
What this bought me
The setup of this was reasonably simple as I already had the shell script I could direct Claude to reuse and set this up. The changes to the data loader and fixture took a bit of time to finetune and test. Disk cost per worktree is trivial because the full dataset exists once and worktrees load small test datasets.
The value I get from this is that I can run multiple AI agents in parallel, each on its own worktree with its own end-to-end test environment. As the complete environment is setup automatically for each worktree, I do not have to remember to assign ports and directories etc. Whenever I forget which ports are applicable to my current worktree direnv show_dump "$DIRENV_DIFF" gives a quick view on the relevant environment variables.
I hope this was useful. Or, if you already have a system in place for end-to-end testing across worktrees, let me know your approach in the comments.