work.draindk.techCounted 22 October 2026

How this site is built

I set the direction and make the calls. Two AI agents write the code, Claude and Codex, in one repository, with a handoff file they both read and checks that fail the build.

01 / Every change

  1. 9 SepCodex’s first version, on ChatGPT Sites
  2. 24 SepClaude’s first change; HANDOFF.md
  3. 26 SepVercel: a push to main is production
  4. 14 OctError reports (Sentry)
  5. 16 OctThe speed walk (perf-site.mjs)
  6. 17 OctThe rules in every build; thread branches stop deploying
  7. 18 OctUnder the hood, measured on every case
Changes on main
729
Since 9 September 2026, merges not counted.
With Claude
678
Each names Claude as co-author, in the public history .
Merges
112
A thread’s branch landing on main.
Days
44
29 with a change.

02 / Who does what

Oliver Kure Olsen

Me

Direction and every call: the idea, the look, what ships and what is cut. The footage and references a piece starts from. The last look, on a laptop and a phone.

Claude

Anthropic · Claude Code

Writes the code, the shaders and the Blender scripts, and runs the checks. One thread per job: code in the cloud, renders and timing on my Mac.

Codex

OpenAI

Built the first version in September and still takes jobs on the same repositories. A case trial goes on a branch of its own and merges when I say so.

Every session reads HANDOFF.md first: what is live, what is open, how I work and the rules for publishing, kept under 12 KB. AGENTS.md holds the standing rules. One agent works on a site at a time, and every session pulls before it starts, because main is production.

03 / From a branch to live

  1. claude/<thread>

    A thread’s branch. It never deploys.

  2. preview/<name>

    A preview link, when one is needed.

  3. tsc --noEmit

    Types.

  4. next build

    Next.js 16, exported as static files.

  5. consistency

    47 rules over every page, the CV and its PDF, in about five seconds. One finding fails the build.

  6. perf-site.mjs

    On the Mac, before a piece ships: the whole visit in one tab, page to page, at a phone’s size with the CPU slowed four times; Safari in the iOS Simulator.

  7. main

    A push is production, on Vercel. A build that changes no page is skipped.

  8. live

    Errors report to Sentry. Every Friday a check reads them, fixes what it can reproduce and runs the rules against the live site, the Maquettes’ repositories and the store.

04 / The rules

47 rules in scripts/consistency/rules.mjs, each a decision of mine with its date and reason. When something shows in more than one place, a page, the CV, a demo or the store, it can’t change in one and stay in another. Ten of them, in their own words:

  • 9 OcttitleOne title on the site: 3D web designer
  • 13 OctwordingNone of the words dropped from his positioning
  • 15 Octbodernes-placeBodernes sits near the beach, not on the sand
  • 17 Octpin-soundThe pin table has no sound and no Sound switch
  • 17 OctcopiesCode kept in two places changes in both
  • 17 Octscan-no-nodThe scan doesn’t nod or bow when he faces you
  • 18 Octmagnus-white-wallMagnus' wall is painted white
  • 20 Octclient-labelsEach client site says what it is: Live site, Redesign or Proposal
  • 20 Octwork-orderWork runs the Simulacra first, then the live sites, then the proposals
  • 21 Octhow-i-workHow I work says who writes the code: AI agents, with him making the calls
The check on a copy of the repository, with the Earth’s axial tilt changed in one of the sun’s two copies (its advice on fixing left off the end)
$ node scripts/consistency/check.mjs
consistency: no out/ (run next build first); checking the sources only.
consistency (build): 281 documents, 47 rules.

✗ copies: Code kept in two places changes in both
    app/showroom/sky.ts  ↔  app/lab/pintable/engine/sky.ts
      the sun and moon: they now differ where they were the same
        only in the first:  const RAD = Math.PI / 180, E = RAD * 23.44;
        only in the second: const RAD = Math.PI / 180, E = RAD * 23.4397;
    Why: Swash’s surf runs under Bodernes, the opening’s table is Pintable’s, the WebGPU yield guard is in three places, and every Maquette page copies its repo’s src/. A fix in one copy and not the other is how the Safari band upload missed Pintable (17 October).
    Owner: the thread that changed the copy. Every copy: docs/consistency-map.md#copies

1 to fix.

05 / Speed

Measured in Chrome on a MacBook Pro (M4 Pro) against the live site: a 390 px phone at 3×, the CPU slowed four times, scrolled to the end by a thumb’s flicks. The GPU is the Mac’s, so a real phone is the last check.

CasePhone95 % of framesOn a phoneDraws with
Terne X60 fpsunder 16.8 ms6.1 MBWebGL2
Horisont60 fpsunder 16.7 ms1.2 MBWebGL2
Grums60 fpsunder 16.7 ms7.2 MBWebGL2
M1 microphone60 fpsunder 16.7 ms4.7 MBWebGL2
Tide60 fpsunder 16.8 ms13.9 MBWebGL2
Bodernes Strandcafé60 fpsunder 16.7 ms2.8 MBWebGPU, then WebGL2
Murermester Magnus60 fpsunder 16.8 ms2.4 MBWebGL2
Severin60 fpsunder 16.8 ms3.0 MBWebGL2
Bageriet B60 fpsunder 16.8 ms2.6 MBHTML and CSS

What the lag taught

  1. A shader compiles in the first frame that draws it. So every piece compiles all of its shaders before its first frame, hidden and culled parts included, and shows a model only once they have settled.
  2. A compile belongs to what it draws into. Warm-ups bind the frame’s own render target; on WebGPU, the frame’s call depth too, shadow maps and post passes included.
  3. Safari waits for a shader still linking inside any call that touches it. Nothing draws before it is warmed, and every program a material has is waited for, not only its latest.
  4. Pictures decode off the page. createImageBitmap for images, a worker for EXRs, big textures uploaded in bands of rows.
  5. Long work on arrival runs in slices of a few milliseconds, each its own task, with a message between them rather than a timer.
  6. A context is released when its piece leaves: dispose(), then forceContextLoss().

06 / Errors

Sentry, in the EU, errors only: no tracing, no replay, no cookies, no IP addresses. A 1.4 KB loader; the SDK comes only with the first report, so a page that works never loads it. A piece that gives up and shows its still says so (fellBack()). The first catch, 14 October: Shrike’s WebGPU texture views failing on Chrome 141, now guarded by guardWebGPU().

07 / Where it runs

Cloud
Claude Code, a session per thread: code, builds, checks and screenshots. No GPU.
Mac
A MacBook Pro, M4 Pro: Blender and Cycles, films, the speed walk, Safari in the iOS Simulator. One GPU job at a time.
iPhone
The last look.

Index 11 projects, 7 in the Lab