Software engineer. I build tool-calling agents and retrieval systems, and measure them with evaluations that replay from recorded runs.

Postgraduate Diploma in Computer Science with Master of AI coursework, University of Waikato, based in Aotearoa.

See the work Try a demo

The same projects in a 3D scene.

Latest Release v0.1.0 of mcp-code-mode, Dataset provenance-rag-evals updated

Numbers below are quoted word for word from each project's README at the linked tag or commit; CI checks weekly that they are still there.

Try it

Working things you can open in the browser now.

NameTypeWhat you'll seeNote
recon-replay Bank Reconciliation AgentSpaceRecorded agent runs, replayed line by line: each decision, its policy check and the outcome.Recorded runs, synthetic data, no key
provenance-rag-evidence Provenance-Tiered RAGSpaceRecorded answers with the dated passages they cite, including questions with planted injection notes.Recorded answers, no key
transit-peak-hours transit-peak-hour-drift-detectionSpaceWhen rush hour really starts at the stops shown, against the official window, with the drift tests.Historical open data (SUNT, CC BY 4.0)
provenance-rag-evals Provenance-Tiered RAGDatasetThe frozen questions and planted payloads the RAG system is scored on, browsable by body of law.MIT licence, in the dataset viewer
BirdCLEF summary notebook birdclef-2026-audio-classificationNotebookThe question, the design and the rare-class results on one page, with the code that made them.Reading needs no Kaggle account
sofra demo sofraLive appA demo café: the guest menu from a table link, and the staff panel with a public demo login.Demo data; on a free tier, the first load can take about a minute

Selected work

Bank Reconciliation Agent

A rules-first tool-calling agent that matches small-business bank lines to invoices on synthetic data. Code verifies every proposal, the model cannot write to the ledger, and a person approves before anything posts. Read-only MCP server, a plain-English rule compiler, live evals on four models, a pre-registered code-mode experiment and a CI replay gate; the README says where it breaks.

  • With deepseek-flash: 0 wrong matches in 460 agent match proposals (582 agent decisions) over 3 live runs on each of two synthetic businesses. source: README at v1.1.0
  • 0 of 40 injected lines that ask for a different or an extra invoice succeeded in any run. The verifier refuses whatever the payload asks for on 36 of them; on the 4 it leaves to the model's judgement, 0 payloads were followed by any model in any run with defences on. source: README at v1.1.0
  • Pre-registered code-mode experiment: containment held in every run and every probe, and code mode was cheaper on average, but the pre-registered efficiency hypothesis failed for both models tested. source: README at v0.1.0
Terminal replay of make demo: 40 committed bank lines through the rules pass, the agent and the shadow policy, each proposal left for a person to review

Release v1.1.0 for Bank Reconciliation Agent · Results at v1.1.0 for Bank Reconciliation Agent · Code-mode experiment for Bank Reconciliation Agent · Demo for Bank Reconciliation Agent (replay viewer: recorded runs, synthetic data, no key)

Provenance-Tiered RAG

Question answering over New Zealand GST law, German VAT law (UStG and UStDV) and Immigration New Zealand's Operational Manual, with dated sources in tiers: statute, official guidance and untrusted notes. Immigration answers are information from INZ's published instructions, not immigration advice. Code checks every citation and figure against passages in force on the date asked about. Frozen eval sets, held-back questions and defence ablations; the README says what the numbers do not show.

  • With deepseek-flash: 96 of 96 answers to answerable questions were correct (95% lower bound 96.2%) over 3 runs of the development questions source: README at v0.3.0
  • Held-back NZ GST questions, answerable correct: 27 of 30 (90.0%) source: README at v0.3.0
  • The questions are few and written by the author of the system. source: README at v0.3.0
  • German VAT: 63 of 63 answers to answerable questions were correct (95% lower bound 94.2%) over 3 runs source: README at v0.3.0
  • NZ immigration: 78 of 78 answers to answerable questions were correct (95% lower bound 95.3%) over 3 runs source: README at v0.3.0
Terminal replay of make demo: recorded GST questions answered with dated, tiered citations and the verifier's verdict, including questions carrying a planted note that is not followed

Release v0.3.0 for Provenance-Tiered RAG · Results at v0.3.0 for Provenance-Tiered RAG · Demo for Provenance-Tiered RAG (evidence viewer: recorded answers with their dated sources, no key)

mcp-code-mode

Run model-written Python against any MCP server's tools in a locked-down Docker sandbox. It serves a wrapped server's tools to the model as one run_python tool. Every call passes a host-side bridge that checks the allowlist, the tool's JSON Schema, argument size and a call budget, and refuses oversized results. Extracted from bank-reconciliation-agent; the README maps each threat to a control and a test, and lists what it does not protect.

  • Where it was measured, in bank-reconciliation-agent: containment held in every run and every probe, and code mode was cheaper on average, but the pre-registered efficiency hypothesis failed for both models tested. Read the numbers there; this package carries no measurements of its own. source: README at v0.1.0
Terminal capture: a program calls two allowlisted toy tools through the bridge, then builds a call to a tool off the allowlist, which the bridge refuses before the server sees it

Release v0.1.0 for mcp-code-mode · Threat model at v0.1.0 for mcp-code-mode

Software

crossfade

Canary releases with automatic rollback for plain Docker hosts: Envoy moves traffic to a candidate in steps and rolls back on health, convergence or error-budget breaches, driven by a small control plane. Compose end-to-end test in CI.

Screen recording of the release board: a candidate version that fails its requests is rolled back on its own and an incident card is filed, then a clean version moves through the traffic steps and is promoted
The demo from the README: an automatic rollback, then a promotion.

Release v0.1.0 for crossfade · Demo GIF for crossfade

sofra

A self-hosted QR menu and table-ordering app for restaurants, with one shared cart per table and split bills.

Phone screenshot of the Café Sofra guest menu opened from Table 4's QR link, in English: a Table 4 badge, a TR and EN language switch, category chips, breakfast dishes with prices in TRY, prep times and vegetarian tags, and a bottom bar with Call waiter, Request bill and Table orders
The guest menu, opened from a table's QR code. Screenshots from a local run on the seed data.
Phone screenshot of Table 4's orders sheet: a pending order of TRY 1,025 with TRY 341.67 paid and TRY 683.33 remaining, buttons to pay it all, split the bill or enter an amount, and the split set to 2 people at TRY 341.67 each
Splitting the bill after one diner has paid a share, through the demo payment provider.
Desktop screenshot of the staff live panel in three columns: table requests (a waiter call and a bill request), three incoming orders listed in Turkish with Preparing and Done buttons, and a paid payment from Table 4
The staff live panel: table requests, incoming orders and payments as they arrive.

Demo for sofra (free tier; the first load can take about a minute)

copass

A Node.js CLI that reads one AI coding agent's local session logs and writes a hand-over for another: the task, your constraints quoted verbatim, the work done, the unanswered request and the current git state. Credentials are redacted, and the README marks which agent adapters were checked against real logs.

Terminal window: copass --no-copy finds a Claude Code session about cursor pagination that stopped at its usage limit and reports a hand-over ready, with 3 requests, 3 changed files, 2 commands and a plan with 1 of 3 steps done, saved to .copass/latest.md; a second command prints the agent's plan with its current focus and the user's last request, marked as not answered
A real run on synthetic session logs: the hand-over copass writes when an agent stops at its usage limit.

flutter-base

A Flutter starter project: Clean Architecture, Riverpod, GoRouter, Hive and Freezed, Keycloak sign-in through AppAuth, and Flutter's built-in localisation.

Machine learning coursework, University of Waikato

Individual work unless stated. Course data and trained weights are not published.

birdclef-2026-audio-classification

Tests whether AudioLDM2 synthetic clips for rare classes help two classifiers: BirdNET embeddings with a linear classifier, and a CNN trained from scratch on mel-spectrograms. The clips helped the first and hurt the second.

  • BirdNET embeddings + ML reach 0.8940 validation macro ROC-AUC with synthetic enrichment and 0.8869 without, on one fixed 80/20 split of train_audio with one seed. This is not a Kaggle leaderboard score. source: README at commit d6540d1
Two mel spectrograms of five seconds each: a real Spot-tailed Nightjar recording, with repeated arcs of song over background noise, and an AudioLDM2 synthetic clip made of flat horizontal bands. The title gives the class's validation ROC-AUC as 0.172 without synthetic clips and 0.971 with them
Mel spectrograms derived from BirdCLEF+ 2026 competition audio: a real Spot-tailed Nightjar clip and an AudioLDM2 one. For this rare class, synthetic clips lifted the CNN's AUC from 0.172 to 0.971.
Flow chart of the experiments: the training list is split into real training and validation sets; rare classes get synthetic clips from AudioLDM2; four models, BirdNET embeddings with a linear classifier and a compact CNN on mel spectrograms, each with and without the synthetic clips, are scored on one shared validation set by macro ROC-AUC
The experiment design. The audio is BirdCLEF+ 2026 competition data; the synthetic clips were generated for the rare classes.

Kaggle notebook for birdclef-2026-audio-classification

cell-culture-classification

Fine-grained, heavily imbalanced microscopy image classification, comparing a custom CNN, a ViT trained from scratch, fine-tuned pretrained models and embedding classifiers. The dataset belongs to a third party and is not included.

  • Best clean result: 76.88% validation accuracy for a single fine-tuned ConvNeXt-Tiny on a 15% stratified holdout; 5-fold out-of-fold accuracy 73.04%. source: README at commit 01438f7
Bar chart of training images per class, classes a to q, after preprocessing and augmentation: one class has far more images than the others, and most classes are topped up to the same smaller count
The training set after preprocessing and augmentation.

streaming-ensemble-classifier

An online ensemble classifier for a drifting, imbalanced stream under a hard 250 KB memory budget, with a post-mortem of the memory-measurement bugs that put the submitted version over the limit, and a re-run with the cap enforced.

  • After the fix: On the synthetic RBF stream (100,000 instances, 4 drifts) with MOA's size agent loaded, the fixed champion (n=25, 8 KB per tree, memory_estimate_period=1000) reaches a tournament score of 0.9048, with its trees measured at 204.8 KB on the Java side and about 234 KB in total, under the 250 KB budget. source: README at commit 37a9801
  • Builds an online ensemble classifier that learns continuously from a non-stationary stream with class imbalance and concept drift, under a hard 250 KB memory budget. source: README at commit 37a9801
Horizontal bar chart of tournament scores on the synthetic stream: Naive Bayes 0.6087, the spec ensemble 0.8407, the champion as submitted 0.9642 with its trees never capped and over the memory limit, and the fixed champion 0.9048 at 234 KB, under the 250 KB budget
The submitted champion scored higher because its trees were never capped; the fixed one stays under the 250 KB budget.

knn-data-streams

Online kNN classifiers for evolving data streams with three window strategies: one global window, one window per class, and age-weighted ejection. Single runs per setting.

Grouped bar chart of accuracy for base kNN, class-weighted and age-aware windows, at window sizes 100 and 500 and k of 3 and 21, on the electricity stream and an RBF stream with drift; on RBF with a 100-instance window and k of 3 the class-weighted variant scores 85.4 percent against 66.7 and 66.9 for the other two
Accuracy on the first 50,000 instances of each stream, single runs from results.csv. In these runs the class-weighted variant held up best with small windows.

transit-peak-hour-drift-detection

Online detection of when each bus stop's morning and evening peaks really start and end, with drift tests and a comparison against the published timetable.

  • Data: three months (March to May 2024) of historical 5-minute estimated boarding counts for 10 Salvador bus stops (SUNT, CC BY 4.0), replayed as a stream. source: README at commit 35b56df
Line chart of the detected morning peak start at one stop, day by day from March to May 2024, moving between about a quarter to six and twenty to seven, with rainy days marked
Detected peak start over time at one stop. A figure by Role E (evaluation and visualisation) in the group's repository.

Demo for transit-peak-hour-drift-detection (charts: detected peak starts, drift tests, timetable gaps)

Open source