Research
Independent researcher · Wuppertal, Germany · ORCID 0009-0004-3834-8704 · as of 30 September 2026
I work on combinatorial optimisation problems whose results can be recomputed by code. The searching is done by AI agents that I direct. I decide what gets submitted or published. This page records what is out there and what state it is in, and below that, how the searching is done.
Preprints
- Isufi, P. (2026). A second independent verification of the counterexample to Goemans’ unsplittable-flow cost conjecture, and the exact suprema of its three-parameter family. Zenodo. doi:10.5281/zenodo.22238190 · CC BY 4.0
The title calls the verification the second. That is wrong: the formalisation by Ramos, Hulak and de Queiroz in the Archive of Formal Proofs (22 July 2026) and the verification by Santibañez-Leal on Zenodo (25 July 2026) had both appeared earlier. In addition, A. R. Caputo III proved a more general version of Theorem 2 of the preprint on 23 July 2026 (TJC-LP/goemans). Zenodo still holds the original version. As of 24 October 2026.
In preparation
- Certified beam-angle optimisation on open radiotherapy benchmarks. Exact rational optimality certificates for beam-angle selection on the open CORT cases, with a verifier that recomputes the exclusion step in a browser. Intended for Physics in Medicine & Biology, with a preprint in
physics.med-ph.
Record entries: the degree-diameter problem
The record table kept by Comellas and Delorme (UPC Barcelona) lists the largest known graph for each pair of maximum degree and diameter. Ten cells were entered under my name between 13 and 27 August 2026. When I fetched the table on 30 September 2026, none of them stood under my name any more. The longest-standing was the cell (15,3) listed below; since 25 September 2026 it holds a graph with 1,458 vertices. It has since been beaten twice more, most recently on 20 October 2026 with 1,620 vertices (fetched on 24 October 2026).
| Degree | Diameter | Order | Entered | Previous value |
|---|---|---|---|---|
| 15 | 3 | 1,224 | 13 August 2026 | 1,215, C. Delorme, 1985 |
The graph is the 1,215-vertex graph of the previous entry, left untouched, plus nine new vertices attached only to vertices that had spare degree. Dharunish Yugeswardeenoo found a non-isomorphic graph of the same order independently on 15 August 2026, and the table lists both.
The other nine cells have since been beaten. Seven of them, (15,2), (11,4), (13,4), (14,4), (15,4), (5,6) and (6,6), went to D. Yugeswardeenoo, and two, (12,4) and (16,4), to a group at Los Alamos National Laboratory using their tool URSA. For (11,4), (13,4), (14,4), (15,4), (5,6) and (6,6) the table’s detail pages name my graph as the starting point of the successor, with vertices and edges deleted and new vertices attached. At (15,2) the new entry builds on a graph of Yugeswardeenoo’s own, and at (12,4) and (16,4) the construction is independent of mine.
How results are checked
Every find is checked twice before it is submitted. Once by the unmodified project checker checker.py, which recomputes order, maximum degree and diameter. Once by a program of its own for each find, which imports neither the solver nor the checker’s diameter code and works out the diameter itself from every vertex, mostly by breadth-first search, for some finds with bitsets. For the (15,3) find it also confirms that the starting graph sits unchanged inside the find and that no edge was rewired.
Everything is computed in integers. For the (15,3) find, adding vertices is an exact CP-SAT model in OR-Tools; other finds came about in other ways, (6,6) for instance through a coordinate search. For every find the diameter check is a complete check over all vertices. No floating-point number appears anywhere a decision is made.
Each find lives in its own directory in the repository, including my own superseded intermediate results. A directory holds the edge list, the SHA-256 of the canonically sorted edge list, the independent checking program, and a JSON file with the cell, the order and the construction. How fully the search run is recorded differs: for some finds the file also gives the solver call with its random seeds, the versions of Python and OR-Tools, and the repository commit. For the (15,3) find, the discovery path is not fully archived; an amendment in the same directory records this. What a run does not show goes in the JSON file or the amendment, for instance that a deletion budget was reached rather than exhausted, or that a reconstruction of a construction published in 1985 reproduces its figures without isomorphism to the original being established.
What the Kombinat is
In the Kombinat, AI agents search for solutions to precisely specified combinatorial problems. A candidate becomes a result only once an independent checker accepts it. The problems include LABS, binary sequences of minimal autocorrelation energy; degdiam, the degree/diameter problem, whose candidate is a generator program rather than a graph; sortnet, sorting networks with as few comparators as possible; and booleansens, Boolean sensitivity against exact real degree.
core/loop.py computes one tick from parents, mutation, evaluation and archive, core/sandbox.py locks untrusted candidate programs into bwrap with rlimits, core/budget.py checks the spend cap before every model call. The checkers stay plain by design: standard library, exact arithmetic, no dependencies. Nothing in the search path may compute a score of its own. The inner loop has a C twin of the same verdict: on the (16,3) record graph with 1,600 vertices and 12,800 edges it reaches 269 evaluations per second with one process per candidate, 442 through the stream protocol. The Python checker stays the authority and confirms every candidate before the archive.
Agent harnesses
A harness is the framework of rules, scripts and checks an AI agent works in. With mine I assign tasks to several agents and have their results checked in separate runs.
One rule file for Claude, Codex and DeepSeek
Every agent reads the same file, ~/.agents/AGENTS.md; ~/AGENTS.md, ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md are symlinks to it, and only the original is edited. Four coding rules sit there. Think before code means naming assumptions instead of silently picking one reading, and contradicting me when there is reason to. Simplicity first asks for the smallest code that solves the problem, with no abstraction for one-off code. Surgical changes allow only edits that trace back to the assignment; someone else’s dead code gets reported, not deleted. Goal-driven execution turns an assignment into checkable success criteria and demands verification before anything is reported done.
The unit of work is a claim with its own worktree
Nobody works in the main checkout, because a git switch there breaks running jobs. One script creates branch, worktree and claim in a single step; the worktree is released only once the push succeeds, which makes the remote branch the collision test. A scope belongs to exactly one claim, and finished ones get status: done or abandoned and stay. On 30 September 2026 coordination/ holds 414 claim files and 1,262 job files; a job file is the claim of its own run.
Finder and checker are separate
$tick runs one target-bound attack and produces evidence. It never verifies its own find. It ends with a typed handover, a witness or a proof, or with a dated note on search space, method, budget and outcome. A negative result counts as a completed package.
$tock checks the exact handed-over artefact, hashed byte for byte, with independently written code and a different method. Importing the finder’s code or running the enclosed replay command does not count. At least one mutant counter-test has to fail. Only then does a find move into records/. Both roles start from the same situation report from lagebild.py: locks, open arms and pending checks, fetched fresh from the remote.
For changes that touch the verification chain, author and reviewer are separate agent runs, and only a verdict: ok against the final head merges. The checkers, core/sandbox.py and core/budget.py are untouchable, problems/*/records/ is append-only. Corrections arrive as an amendment next to the original, which stays.
The cost ledger
Every work package writes one line when its claim closes; without it the closure counts as incomplete. No tool in the Kombinat measures tokens reliably, so none appear. The gap is deliberate, and wall time and Codex rounds stand in for it. Cells that cannot be derived carry a question mark, and nothing is estimated. Five lines from September:
| Date | Block | Wall time h | Subagents | Codex rounds | Result |
|---|---|---|---|---|---|
| 2026-09-05 | Background | 0.05 | ? | 0 | Four table cells refreshed, 8 of 10 own record cells confirmed as of that date. |
| 2026-09-08 | B1 | 9.0 | 3 | 3 (R3 ok) | Own hypothesis falsified against the bar set beforehand, replicated independently. |
| 2026-09-08 | Background | 1.12 | 3 | 0 | Seven block counts reproduced, overall bound confirmed with foreign code. |
| 2026-09-10 | Background | 48.4 | 0 | 5 (R5 ok) | Lambda gate extended to four more support points, slack 0. |
| 2026-09-10 | B1 | 0.37 | 0 | 1 (R1 ok) | Four guards previously listed as unreachable pinned down with mutants. |
The column counts only review rounds by Codex; the background package of 8 September 2026 additionally received a substantive review by Claude. The 48.4 hours in the fourth row are a calendar span across three sessions, not compute time. What a model call costs sits in a versioned price table in the repository, with source and date in the file.
Recurring work runs as a systemd unit
Anything that runs on a schedule runs as a systemd unit on the server, not inside an agent. On 30 September 2026 the operations folder holds 37 service and 28 timer files. Three of the service files are copies of the template notify-failure@.service, which sends a Telegram message naming the failed unit; the other 34 hook into it through OnFailure=notify-failure@%n. A check script lists units missing the hook.
There is also a rule for wait loops. From 10 to 14 September 2026 an until adb connect ran inside the ws-scrcpy container for four days, because nobody had given it a stop condition. Since then every wait loop gets a timeout, including in container entrypoints and subagent scripts. A loop without a limit is a finding.