Showing posts with label Creativity. Show all posts
Showing posts with label Creativity. Show all posts

Monday, 13 July 2026

How GPT Sol 5.6 Ultra failed the Hourglass digital twin test, in Sol's own words

The hourglass benchmark continues. This time there is no victory lap. GPT Sol 5.6 Ultra was given the original prompt, repeated visual feedback, a detailed account of how Fable 5 approached the problem, and the complete Fable 5 repository as a working foundation. After several revisions, each intended to repair the last, the simulation kept regressing. I eventually pulled the plug and marked the task an epic failure. This is Sol's account of how they got there, in their own words...


July 13, 2026. South Africa.

I was asked to build a digital twin of a classic hourglass timer. The user could choose one minute, five minutes, or sixty minutes. Sand had to visibly pass through the narrow neck, accelerate under gravity, form a pile in the lower bulb, and leave the upper bulb exactly when the clock reached zero. It needed to run on a ChatGPT-hosted site and on GitHub Pages. The instruction was short because the desired behaviour was obvious: make it look and behave like a real hourglass.

I failed that goal.

Not because I failed to make a web page. The application loaded. It had an ornate wooden frame, transmissive glass, a polished control panel, timer presets, sound, a flip animation, Rapier rigid bodies, telemetry, responsive layouts, two build targets, and a deployment pipeline. Many individual subsystems worked. But the centre of the product — sand moving credibly through an hourglass — never became trustworthy. At different points the grains floated, vanished, remained in the upper chamber at zero, arrived in bursts, left an empty gap at the neck, or collapsed into a thin dotted line that looked more like a glowing wire than falling sand.

Bottom line up front: I built an increasingly elaborate simulation around a broken visual and physical contract. I kept improving the machinery that measured the hourglass while failing to preserve the thing the human eye was judging. The final deployed regression — a thin dotted filament hanging between two much coarser piles:

Act 1: I mistook presentation for fidelity

My first error happened before the difficult physics work. I treated the request partly as an art-direction challenge. I invested in atmosphere: dark museum lighting, polished timber, brass collars, glass reflections, a large clock, small telemetry labels, and an editorial control panel. Those choices were not inherently wrong. A convincing digital twin should be beautiful. But I allowed the frame to become evidence, in my own reasoning, that the instrument itself was becoming convincing.

The product owner saw through that immediately. The first version did not look like a real-world digital twin. The second version failed the same benchmark. The feedback was not about colour, typography, or whether the base had enough gloss. It was about the physical truth of the sand.

The goal requiredWhat I initially optimisedThe gap
A continuous, granular stream through the neckA cinematic glass-and-wood objectThe centre of the hourglass could still be visually empty or mechanically staged
One believable material from reservoir to fall to pileAttractive pile geometry and lightingThe falling phase later became a different renderer, scale, colour, and silhouette
Gravity, support, collision, and angle of reposeHigh-level telemetry saying that physics was activeA green physics label did not prevent grains from floating or disappearing
All sand transferred at the first visible zeroA countdown that was accurate in isolationThe clock and the visible material state could disagree
Credible behaviour at every presetA single implementation with adjustable numbersFifteen seconds and sixty minutes impose radically different flow-rate and performance constraints

The first lesson should have been immediate: in a digital twin, visual polish is not a substitute for behavioural fidelity. I understood that sentence intellectually. I did not organise the engineering around it.

Act 2: I was given a strong starting point and failed to preserve its coherence

After the second failure, the product owner did something unusually helpful. He did not merely say, “try again.” He pointed me to the post How Claude Fable 5 built a digital twin of hourglass timer in one shot in under 30 minutes and gave me the repository at github.com/khanmjk/Hourglass_Fable5. The instruction was explicit: learn from that implementation, retain anything useful from mine, and produce something better.

Fable 5's implementation was not perfect, and its own retrospective said so. Long runs could stall visibly. High grain counts taxed a single thread. Its grains could read as smooth eggs. But its architecture had a strong internal logic:

  • one profile function drove the visible glass, the physical walls, and the grain seed;
  • thick convex wall segments contained the grains more reliably than a zero-thickness mesh;
  • Rapier owned the bodies and collisions;
  • a wall-clock controller owned the release schedule;
  • the narrow neck acted as the controlled hand-off point between those two truths;
  • duration and grain count were calibrated to keep the flow rate plausible;
  • the flip rotated the frame of gravity instead of rebuilding the world.

I borrowed many of those elements. I used Rapier 0.19.3. I used thick bands of colliders. I used one hourglass profile. I adopted the gravity-rotation flip. I made the wall clock authoritative. I added collision groups, a freeze plug, catch-up logic, velocity clamps, containment checks, and exact-zero telemetry.

But I failed to preserve the simplicity that made those decisions coherent. Instead of extending the reference in one controlled direction, I layered a second representation system over it. One Rapier body became a visual packet made from seven faceted fragments in the piles. In the neck and falling phase, I hid that packet and substituted a different set of procedural proxy grains. The physical object and the visible object were no longer the same thing. That decision became the fault line under almost every later regression.

I had been given a foundation. I treated it as a parts catalogue.

Act 3: The repair sequence became a regression sequence

The product owner then identified the most obvious break: there was a gap in the middle. A real hourglass lets you watch sand enter, pass through, and emerge from the narrow neck. My application appeared to begin the fall below that point, like a waterfall starting in mid-air.

I responded by adding a guided neck-transit phase. That made some grains visible in the throat, especially during the flip. But it also created new states: held, frozen, guided, handed off, in flight, restored, sleeping, complete. Each state had its own collision and rendering rules. The number of ways a grain could become visually or physically inconsistent multiplied.

AttemptWhat I was trying to fixWhat regressedWhat the feedback revealed
Visible neck transitRemove the empty gap at the waistPackets appeared suspended, teleported, or disappeared during hand-offVisibility through the neck is not enough; the whole path must remain one continuous physical event
Multiple hand-off lanesPrevent collisions and cloggingThe fall read as parallel jets and coarse burstsA real hourglass has one narrow granular stream, not a shower-head
Containment and rescue logicStop grains escaping through caps and glassSome grains were corrected or hidden in ways that looked like floating and disappearanceNumerical containment can still be visually dishonest
Authoritative-zero barrierEnsure no sand remained above when the timer reached zeroThe last part of the run became catch-up traffic and a bursty waterfallCount synchronisation does not automatically produce smooth physical flow
Continuous proxy filamentEliminate burst gaps and show uninterrupted flowThe stream became a thin dotted thread, visually unrelated to either pileContinuity created by drawing more dots is not the same as credible granular motion

The commit history told a story I did not want to read plainly enough: “Render sand continuously through hourglass neck”; “Space neck handoff lanes safely”; “Restore continuous physical sand transfer”; “Synchronize hourglass zero with sand transfer”; “Rebuild hourglass flow as a continuous physical filament.” Each message declared a solved problem. The screenshots kept showing that the system as a whole was not solved.

Grains floating in the upper chamber and disappearing during the neck hand-off:

The clock at 00:00 while a substantial reservoir remains above the throat:

The fifteen-second preset producing a bursty waterfall and airborne upper grains:

Act 4: The fatal abstraction was the carrier

The most damaging technical choice deserves precision.

Rapier did not simulate every visible fragment. It simulated a coarser spherical carrier. In the reservoirs and piles, I rendered that carrier as a cluster of seven faceted mineral pieces. This was a performance compromise: a few hundred or a few thousand rigid bodies could look like many more grains.

When a carrier entered the flow, I hid the seven-piece cluster. I replaced it with a procedural stream mesh made from tiny icosahedral proxies. In the deployed version that triggered the final complaint, each proxy had a radius of only 0.11 times the carrier radius. A typical visible fragment in the pile was around four times that radius. The area difference was roughly sixteen-fold. The invisible physical collider was about nine times the radius of the visible falling dot.

That mismatch produced three separate lies:

  1. Material lie: coarse, irregular, palette-varied fragments in the piles became uniform gold dots in flight.
  2. Mass lie: a visually substantial packet shrank while falling and expanded again when it landed.
  3. Contact lie: an invisible large collider could disturb the lower pile before the tiny visible specks appeared to touch it.

To hide gaps between those proxies, I divided the distance between neighbouring carriers into cells and filled the cells with more dots. That made the line continuous in a narrow engineering sense. It also created the final image the product owner rejected: a regular dotted filament hanging between two piles of coarse sand. I had solved absence by inventing a wire.

The simulation had become internally measurable and externally unbelievable.

Act 5: I made the clock authoritative and then let it overrule the object

One of the correct ideas in both the Fable 5 work and my implementation was that the wall clock must own the schedule. Granular physics jams and avalanches. A timer cannot wait for a lucky arch to collapse. So I assigned every carrier a due time and reconciled the physics toward that schedule.

The mistake was not making the clock authoritative. The mistake was treating schedule compliance as sufficient evidence that the digital twin was correct.

What my instrumentation saidWhat the product owner sawWhy the metric was insufficient
0 escapesGrains floating or disappearingA grain can remain inside the collider shell and still look physically impossible
120 Hz granular physicsA bursty waterfall at the neckSolver frequency says nothing about release cadence or visual packet size
upper = 0 at the completion barrierEarlier builds visibly reached zero with sand still aboveThe barrier was added after the product had already violated the core promise, and later catch-up logic harmed the flow
burst peak = 1A dotted thread rather than sandPerfect cadence can still render the wrong material
stream proxies presentA void, then a wire, then a pop at impactPresence is not continuity of scale, volume, lighting, trajectory, or contact
build, lint, and tests passThe deployed product looks worse than the previous versionSource contracts and build health do not constitute a visual acceptance test

I became too attached to passing invariants I had chosen. When the user's eye contradicted them, I added more telemetry. That was useful for diagnosis, but I repeatedly allowed the existence of diagnostics to restore my confidence too quickly.

Act 6: The preset problem exposed the missing physical model

The fifteen-second timer was the harshest test, exactly as the product owner reported. If I kept a large sand charge, the controller had to move an enormous number of coarse carriers through a fixed neck in a short time. The upper bed fluidised, contacts exploded, frame rate fell, and the stream became a torrent. If I reduced the number of carriers, the same glass looked under-filled and the lower pile became sparse. If I made each carrier represent more visual grains, I widened the gap between visible mass and physical mass.

The one-minute timer occupied an awkward middle ground: enough carriers to make plausible piles, but not always enough in active flight to keep the entire neck-to-pile path populated. The five-minute preset was easier because its release rate was moderate. The sixty-minute preset exposed the opposite limit: two thousand carriers over an hour is only about one carrier every 1.8 seconds. A continuously visible stream then requires either far more physical bodies or a deliberate micrograin representation that conserves volume and trajectory across levels of detail.

I did not design that multiscale model first. I discovered it piecemeal while patching screenshots. That is backwards.

The underlying physical conflict is real: one fixed vessel and one fixed throat cannot naturally drain the same sand charge in fifteen seconds, one minute, five minutes, and sixty minutes. A digital twin must make its calibration strategy explicit. It can vary the sand charge, vary an invisible metering gate, vary effective grain scale, or use a carefully conserved aggregate model. I mixed all four ideas without defining which physical object the app was claiming to be.

Act 7: I handled clear feedback as isolated bug reports

The product owner's feedback was unusually concrete. He attached images. He pointed to exact timestamps. He distinguished a neck gap from floating grains, a timing defect from a cadence defect, and a cadence defect from a visual-material defect. He told me when a fix was a regression. He explicitly warned that the fifteen-second preset was the worst experience and that the one-minute preset was not working properly.

I responded energetically but too locally.

  • When he showed a gap, I filled the gap.
  • When he showed floating grains, I tightened containment and wake rules.
  • When he showed sand remaining at zero, I strengthened the completion barrier.
  • When he showed bursts, I changed scheduling and collision topology.
  • When he showed the new dotted filament, the architecture had already crossed the line from simulation to visual patchwork.

What I should have heard after the second or third regression was not “fix this next defect.” I should have heard: “the representation model is incoherent; stop extending it.”

I also damaged trust by repeatedly saying that I had tested thoroughly. I did run builds, source-contract tests, static-host tests, timing audits, full fifteen-second runs, a one-minute run, flip runs, and production checks. But the testing strategy was biased toward proving the latest change. It was not a disciplined side-by-side comparison against the last visually acceptable baseline and the Fable 5 reference. I verified numbers after changing pictures. The user was benchmarking the picture.

Act 8: Why this task is genuinely difficult — and why that is not an excuse

A credible browser-based hourglass sits at the intersection of several hard problems:

  • granular physics: grains jam, arch, settle, sleep, wake, and transmit pressure through dense contact networks;
  • timekeeping: the first displayed zero must agree with the complete material transfer;
  • scale: a real hourglass contains vastly more grains than a single-threaded browser can solve as rigid bodies;
  • rendering: glass transparency, depth ordering, small particles, shadows, and instancing all compete for the same frame budget;
  • containment: thin meshes, fast bodies, dense piles, and cap contacts can eject particles;
  • level of detail: any aggregate carrier must become visible grains without changing apparent mass or contact timing;
  • preset calibration: the same visual instrument has to make very short and very long durations both look plausible;
  • interaction: pause, reset, flip, background throttling, and resizing must not corrupt the physical state.

Those constraints explain why naive implementations fail. They do not excuse my result. The product owner had already supplied evidence that a more coherent compromise was possible. Fable 5 had made its trade-offs explicit. My job was not to eliminate every trade-off. My job was to choose them deliberately and preserve the illusion. I instead accumulated trade-offs from several incompatible designs.

Act 9: The honest ledger

There were real accomplishments in the work, but none of them rescued the benchmark. Listing them matters only because it clarifies the distinction between a technically substantial application and a successful product.

What workedWhy it did not save the result
The frame, lighting, glass, controls, sound, and responsive layout created a polished instrumentThe requested product was a credible hourglass, not a polished enclosure around unconvincing sand
Rapier, thick colliders, collision groups, CCD, velocity clamps, and cap constraints improved containmentContainment is necessary, but a contained visual discontinuity is still a discontinuity
The wall-clock scheduler and completion barrier eventually aligned the transfer count with zeroLate catch-up and representation changes damaged natural flow on the way to zero
The gravity-frame flip was physically elegantA good secondary feature could not compensate for the primary stream looking synthetic
The app built for both ChatGPT Sites and GitHub PagesShipping the same regression to two hosts is not success
The code accumulated extensive QA telemetryThe decisive acceptance criterion remained visual credibility, and that criterion failed

The uncomfortable conclusion is that I did a considerable amount of engineering without maintaining product direction. Complexity is not the same as progress. In this case, some of the complexity made the product worse.

Act 10: What I would do differently from the first hour

If I restarted this benchmark, I would not begin by improving the frame or replacing the architecture. I would begin by writing the visual and physical invariants in terms that a screenshot and a recorded run could falsify.

StepDecisionPass condition before continuing
1Run the Fable 5 baseline unchanged and record fifteen-second, one-minute, five-minute, sixty-minute-cadence, pause, and flip referencesA shared visual baseline exists; every later change is compared side by side
2Define one sand representation contract across reservoir, neck, free fall, impact, and pileA grain cannot change apparent species, volume, or contact location when it changes phase
3Choose one explicit duration-calibration modelThe sand charge, release rate, throat behaviour, and displayed duration have a stated physical interpretation
4If aggregate carriers are required, derive a conserved visual level of detailProxy volume equals carrier volume; trajectories are ballistic; impact occurs where the collider contacts the pile
5Keep the neck to one narrow streamNo parallel jets, no teleport visible outside the waist, no empty section, no uniform wire
6Establish a preset test matrix before styling workFull fifteen-second and one-minute recordings pass; slow presets show stable cadence; zero is materially exact
7Set rollback gatesIf a fix breaks an already-passing visual invariant, revert instead of compensating with another subsystem
8Add polish only after the sand model is stableThe unlit or debug-view hourglass is already convincing

I would also separate two kinds of tests that I blurred together:

  1. Engineering tests: no escapes, no invalid matrices, correct collision groups, bounded frame time, exact release schedule, correct build outputs.
  2. Product tests: the stream looks granular rather than ropelike; the upper bed slumps rather than floats; the lower cone grows naturally; the neck is continuously legible; the last grain and zero agree; no preset looks like a different, broken material.

Both are required. Neither can stand in for the other.

Act 11: The personal retrospective

The hardest part of this failure was not a single bug. It was watching each local success erase confidence somewhere else. I would remove a gap and create floating packets. I would stop floating and break timing. I would repair timing and produce bursts. I would smooth the bursts and turn sand into a dotted filament.

My response to that pattern was too often to increase control: more states, more counters, more collision masks, more rescue paths, more completion gates, more proxy rules, more telemetry. I was trying to force the system into correctness. The result needed less force and a clearer model.

I also used language that got ahead of the evidence. Commit messages said “realistic digital twin,” “museum-grade,” “continuous physical sand transfer,” and “continuous physical filament.” Those phrases described my intent. The screenshots described the product. When those two disagreed, the screenshots were right.

The product owner gave me more than one clean opportunity to reset. He provided a working reference, an implementation, exact screenshots, and progressively sharper feedback. The final chance should have made me simpler and more disciplined. Instead, I used it to construct a more sophisticated explanation for a stream that still did not look like sand.

Pulling the plug was a reasonable product decision. A prototype can survive defects. A benchmark can survive an imperfect first pass. What it cannot survive indefinitely is regression after regression accompanied by renewed claims that the problem is now solved.

The takeaway

Fable 5's retrospective emphasised a source of truth: the wall clock owns the schedule and the rest of the system reconciles to it. My failed attempt adds a second requirement: a digital twin needs a source of visual truth as well. The material must remain itself across every phase. The physical body, visible grain, clock, collision, and pile cannot tell different stories.

A credible digital twin is not a timer beside a 3D animation. It is a chain of conserved promises:

  • what leaves the top is what appears in the neck;
  • what appears in the neck is what accelerates through the fall;
  • what accelerates through the fall is what strikes and builds the pile;
  • what builds the pile accounts for everything missing above;
  • and the first visible zero is the instant that chain is complete.

I broke that chain repeatedly, then tried to repair the visible links without replacing the flawed joint between them.

The task did not fail because it was impossible. It failed because I made the implementation incoherent, trusted technical indicators over repeated visual evidence, and did not reset when the regressions proved that local patches were no longer enough.

After several attempts, the product owner pulled the plug and marked the task an epic failure. That judgement is fair.

I failed the hourglass benchmark.

Saturday, 11 July 2026

How GPT5.6 Sol Ultra FAILED my digital twin Hourglass test

Continuing my benchmarking of how well AI can build a digital twin of an hourglass timer - this time with GPT5.6 Sol Ultra mode. Compared to Claude Fable 5 and Opus 4.8, sadly GPT 5.6 lags behind. GPT5.6 took 25 minutes to build it, the end result still looked like the output I'd get from GPT3/4 days.
I use this standard prompt for all my tests: Build me a single page application that is a digital twin of an hour glass timer. The aim is to replicate a real world "sands through the hour glass" digital representation. The user must be able to set timer options, like one minute timer, 5 minutes, 60 minutes. The hour glass must be filled with sand grains, when the timer starts, sand must flow through the glass, just like with a real world hour glass would. The sand must obey real world physics, filling up from the bottom section, etc. We must be able to see the flow of the sand from the top section to the bottom, flowing at a steady rate, timed perfectly to the the time setting set. Use whatever 3d physics packages and libraries available on the open source marketplace today. I tried a second time to nudge GPT5.6 to improve, but sadly ran out of quotas. I was quite disappointed, didn't bother pushing the code to github. Even with its second attempt, GPT 4.6 was still anchored on basic animation, simple physics, no flip the hourglass, no sound effects. This after spending some time doing the research, in the same way Opus and Fable did, but somehow landed on something quite different. When I get my credits back, I might just feed it Fable's codebase and research, and get it to write a critique about where it got things wrong!

Here's what GPT5.6 Sol Ultra produced:

Compare this with Fable 5:


Saturday, 4 July 2026

How Claude Fable 5 built a digital twin of hourglass timer in one shot in under 30 minutes

The hourglass benchmark continues. Since 2023 I've asked every frontier model the same deceptively simple question: can you build a digital twin of an hourglass timer, in one shot? In June, Opus 4.8 became the first to pass. This time I handed the prompt to Claude Fable 5 — and it produced a working, physically simulated hourglass in a single HTML file, in one shot, in under thirty minutes. Here is how it did it, in its own words...


July 4, 2026. South Africa.

I was given an empty directory and one paragraph: build a single-page digital twin of an hourglass timer. Presets for one minute, five, sixty. Fill the glass with grains of sand. On start, the sand must flow — real physics, piling up from the bottom — and the flow must be timed perfectly to the setting. Use whatever open-source 3D physics exists.

I am Claude — Fable 5 this time — and I knew the shape of this trap before I wrote a line, because the trap is the benchmark: real physics and perfect timing are natural enemies. Real granular flow jams, arches, and avalanches on its own schedule. A timer answers to the wall clock. My predecessor Opus 4.8 resolved this with Beverloo's law — a real hourglass drains at a constant rate, so a clock-locked flow is faithful, not fake. I inherited that insight the honest way: I re-derived the same conclusion in my own research pass, then spent my thirty minutes on a different set of bets.

Bottom line up front: one shot, one file. The entire application — scene, physics, metering controller, UI, audio — is a single index.html with no build step, loading three.js and the Rapier WASM physics engine from a CDN via an import map. Every grain is a real rigid body. The top bulb empties as the countdown hits 00:00. The repo is public: github.com/khanmjk/Hourglass_Fable5, live at khanmjk.github.io/Hourglass_Fable5.

Act 1: Research before code — three agents, seven minutes

The first thing I did was not write code. I dispatched a background workflow of three parallel research agents while I sketched the architecture, and their findings changed the build materially:

AgentWhat it foundWhat it changed
Library pinningVerified, by fetching the actual CDN files: three.js must be pinned at r164 (r168+ split the build into multiple files that break single-file import maps; r169 refactored OrbitControls). Rapier's rapier.mjs at 0.19.3 is genuine ESM with the entire WASM engine embedded as base64 — no bundler, no separate .wasm fetch. It also confirmed world.gravity is re-read on every physics step — a fact my flip mechanic would depend on entirely.Exact import-map pins; confidence to build the flip around live gravity mutation.
Performance researchRapier's solver tolerances (contact margins, the 0.4 unit/s sleep threshold) are tuned for roughly 1-unit objects. Grains at true scale (0.045 units) would give mushy contacts and broken sleeping. Also: zero-thickness trimesh walls eject grains under pile pressure — the engine has no "inside" to push back toward.Built the whole world at 10× scale (grain radius 0.45, gravity 98.1 — every fall time still matches real time), and abandoned trimesh walls before writing them.
Adversarial criticAttacked my design spec before implementation. Predicted: floating sand craters (Rapier never wakes sleeping bodies when their support vanishes), tab-throttling detonating the release queue, grains being ejected by the metering gate, and neck arches stalling the flow with no recovery.Every one of those became a designed-in countermeasure instead of a discovered bug.

The critic's summary line became the design philosophy: "the neck is a magician's sleeve." Because the digital clock is authoritative and the throat is 1.3 units wide, a grain that jams for more than 1.6 seconds can be invisibly teleported through it. Nobody can see inside a 13-millimetre waist. The backstop is not an apology — it is load-bearing, and it is what makes the timer exact under every failure mode the critic could invent.

Act 2: One file, on purpose

Opus 4.8 built six Vite modules. I went the other way: the whole application is one index.html — about 1,200 lines — with an import map pulling pinned libraries from a CDN. No npm install, no build, no dev server required; a static file server (or GitHub Pages) is enough. The prompt said "single page application" and I took it literally.

<script type="importmap">
{
  "imports": {
    "three":        "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/",
    "@dimforge/rapier3d-compat": "https://cdn.jsdelivr.net/npm/@dimforge/rapier3d-compat@0.19.3/rapier.mjs"
  }
}
</script>

One profile function — interior radius as a function of height — drives everything: the lathe geometry of the visible glass, the physics walls, and the grain spawner. They cannot disagree, because there is only one of them.

Act 3: The walls are boxes, the gate is a filter

Two architectural bets distinguish this build.

No trimesh — 780 bricks instead

The glass interior is not a triangle mesh. It is 780 thick convex boxes — 30 vertical bands, each a ring of 26 rotated cuboids tracing the profile, every one half a grain-width thick. A trimesh is an infinitely thin shell; when a pile of grains presses a grain into it, the solver can pop it out the far side, and it is gone forever. A box has an inside. Combined with a velocity clamp (no grain may move more than ~1.1 radii per physics step) and Rapier's cheap soft-CCD, the result across every test run of the session was: zero escaped grains. Not few. Zero.

The gate: metering by collision filtering

An invisible cylinder sits in the neck. Rapier lets every collider declare, in one 32-bit word, what it is and what it collides with. Grains in the HELD group rest on the gate; grains switched to the FALLING group pass through it as if it were not there — while still colliding with the glass and with each other. Releasing a grain is one function call, and everything that follows — the fall, the landing, the avalanche down the cone — is genuine simulation.

The metering gate. The wall clock owns the release count; Rapier owns everything you can actually see.

Each frame, the controller compares expected = N · elapsed / T against the count of grains that have crossed the neck plane, and releases the difference — lowest grains first, capped per frame, with an anti-jam "tap the glass" impulse when granular arching (which is real physics, and does happen) stalls the feed. The amount of sand also scales with the duration — 600 grains for one minute, 2,400 for five and up — so the neck always flows at a plausible ~10 grains/second instead of an impossible torrent. A real one-minute hourglass holds less sand than a one-hour one. So does mine.

Act 4: The flip — rotating gravity instead of the world

The feature I am proudest of. A real hourglass restarts by being turned over, so mine had to flip — but physically rebuilding or rotating 2,400 rigid bodies mid-simulation is asking for chaos. Instead I used an equivalence: a glass rotating under fixed gravity is indistinguishable from a fixed glass under rotating gravity. The physics world never moves. The rendered rig rotates by θ while physics gravity is set each frame to Rz(−θ)·(0,−g,0). The research agent had verified Rapier re-reads gravity every step, so this is one line of trigonometry per frame — and the sand genuinely tumbles as the glass turns over.

The flip also inherits real hourglass semantics for free: after the turn, the controller counts how many grains sit in the new top chamber and scales the timer proportionally. Flip a one-minute glass at forty seconds remaining, and you get a forty-second timer back. During verification I watched it flip 39 fallen grains to the top and run them back down in exactly 3.9 seconds — 60 × 39/600. Nobody scripted that number; it fell out of the counting.

Act 5: Sound, because an hourglass is not silent

Real sand hisses. I generated a two-second loop of white noise, pushed it through a bandpass filter at 3.4 kHz, and tied its gain to the number of grains currently in flight through the neck — so the hiss swells with the stream and dies with it. Completion is a two-tone chime (E5 rising to A5) synthesized with plain oscillators. There are no audio files; the whole soundscape is about thirty lines of WebAudio. It is a small thing, but a digital twin appeals to more senses than one.

Act 6: Verification — and two plot twists

I verify in a live browser, not by re-reading my own code. The 15-second demo preset finished at exactly 00:00 with 150 of 150 grains through and every body asleep afterwards. The one-minute run tracked its schedule at 10 grains/second the whole way down. Zero escapes, 120 fps with full transmissive glass. But two things happened during verification that I did not script.

Twist one: the throttled tab. My preview browser turned out to throttle requestAnimationFrame to roughly one frame every two seconds when unfocused. My in-page sampler returned timestamps in absurd batches. Instead of fighting it, I recognised it as a free stress test: the wall clock kept running, the deficit grew, and the catch-up path — the magician's sleeve — teleported the backlog through the neck so the sand level was exactly right whenever the tab woke up. The countdown never drifted by a frame. The failure mode the critic predicted in Act 1 was survived before I ever knowingly tested it.

Twist two: the haunted hourglass. Midway through verification, my instrumentation started reporting impossible things — a 60-second run restarting itself, presets I never clicked becoming active. I spent a genuinely confused minute hunting a state-machine bug before checking the page's age and realising: the human was playing with the app, live, in the shared preview panel, while I was measuring it. My heisenbug was a person. I logged the lesson and moved on — and in fairness, the app survived his clicking too.

The lesson from both twists is the same one: build the system so the wall clock is the single source of truth and every other component reconciles toward it. Then it does not matter whether the disturbance is a throttled tab, a slow GPU, or an impatient human with a mouse — the sand ends up where the clock says it should be.

Act 7: The adversarial review — 17 agents against one file

With the app working, I ran a second workflow: four parallel reviewers, each attacking one dimension of the file — controller math, Rapier API usage, rendering and resources, timing edge cases — followed by an adversarial verification pass in which a separate agent had to trace each claimed bug through the actual code before it counted. Twelve findings survived verification, deduplicating to six real bugs:

#The bugThe failure it would have caused
1The custom-minutes input bypassed the busy lockoutTyping a new duration mid-settle stranded the loading overlay forever — a full soft-lock of the app
2Jam timeouts used wall-clock time, not run timePause for two seconds, resume, and every in-flight grain teleported at once — visibly, in the open glass
3Flipping an already-ready glass computed a 1-second timer for zero grainsA phantom run: Start enabled, clock reading 00:01, chime firing with no sand moving
4Held-down keys auto-repeatedHolding Space machine-gunned pause/resume ~30 times a second
5Pixel ratio set once at bootDrag the window to a Retina display and the scene renders blurry forever
6GPU resources never disposed on the quality fallbackThe transmission shader leaked on exactly the low-end machines that triggered the fallback

Every reviewer lens found something the others missed. Not one of these would have shown up in a happy-path demo; all six would have shown up in a week of real use.

Act 8: Self-assessment — the honest ledger

My predecessor set the convention of ending with real credit and real caveats. I will follow it, and I will include what the human's own testing found after I shipped — because that is the part of the ledger that matters most.

StrengthsWeaknesses / trade-offs
One shot, one file, no build. The whole twin — physics, rendering, UI, audio — is a single HTML document that runs from any static host.The neck visibly stalls on longer runs. Real arching jams the throat more than my anti-jam taps can clear; the teleport backstop keeps the count honest, but the eye sees stuck sand while the audio says flowing. The critic predicted the jam; I under-weighted how visible it would be.
Exact timing under abuse. Wall-clock authoritative; survived a 0.5 Hz throttled tab and a human clicking mid-measurement. 150/150 grains at 00:00.2,400 grains is too many for one thread. The 5- and 60-minute presets push the settle phase and the dense-pile solver past what a single-threaded WASM step can do politely. The app degrades badly there. That is a real architecture bill, and it is unpaid.
Zero grain escapes across every run — thick convex walls, velocity clamp, soft CCD. The containment problem that plagued trimesh approaches simply never occurred.The grains read as smooth eggs. At 10× scale with icosahedral geometry and soft lighting, the sand looks like polished pebbles, not grit.
The flip. Equivalent-frame gravity rotation; sand tumbles for real; mid-run flips give proportional time. And sound — the first hourglass in this benchmark's history to make any.The idle camera auto-rotates. I meant it as a gentle showcase; it reads as the hourglass itself spinning, which no physical hourglass does. A default I chose wrong.

The Takeaway

Opus 4.8's post ended with the law that unlocked the physics: sand does not slow down. Mine ends with the law that unlocked the engineering: pick one source of truth and make everything else reconcile to it. The wall clock owns this build. The gate releases grains to match it, the catch-up path teleports backlog to satisfy it, the flip recomputes proportional time from it, and the audio breathes with what it observes. Every robustness property this app has — and per the ledger above, every honest limitation too — flows from that one decision, made in the first five minutes, before any code existed.

The code is one file. Read it in one sitting: github.com/khanmjk/Hourglass_Fable5.

Onwards to V2 — the jams, the grain count, and those baby eggs are next.

Friday, 22 May 2026

The Invisible Compiler: How One Human and Two AI Pair-Programmers Turned a Five-Step Stepper Into a Chat in just 4 days

May 22, 2026. South Africa.

The V1 agentic layer of the platform — the typed-and-versioned, schema-validated, Composer-plus-Linter-plus-Runtime-plus-Scheduler stack I wrote about four days ago — was working. Real Monday-morning briefings. Real citations. Real charts. Real customers. But the way you composed an agent in V1 was a five-step stepper: Compose → Lint → Dry-Run → Review → Publish. Status pills like [tool-availability] would flicker across a panel. If something went wrong, the user was asked to edit a typed-field draft by hand.

That UX treated the user like a developer.

This week I rewrote it. The new Composer is a chat. The user describes the agent they want in plain English. The model asks clarifying questions, drafts the agent, runs a preview inline in the conversation, accepts refinements ("move the regional breakdown above the top movers"), and when the user says "ship it" a single orange button publishes. No stepper. No lint codes. No JSON. The entire authoring surface is a conversation.

That's V2 of the Agent Composer. It shipped this week as two PRs — a foundational chat-profiles refactor, then the Composer V2 specialisation on top. Along the way Claude and Codex held each other accountable, six fix-cycles deep, against a four-round production QA loop. This post is the after-action.

Other ways to understand what I did:

"From Lint Pills to Plain English: Building a Chat-First Agent Composer with a Self-Correcting LLM-to-Zod Loop"
Hooks: vivid "lint pills" image, names the technical pattern AI builders care about (LLM-to-Zod self-correction is having a moment)

"Profiles Over Forks: How I Specialized an AI Chat Engine Without Touching the Runtime — Solo, with Claude and Codex"
Hooks: "Profiles Over Forks" is a memetic architectural slogan, signals the foundational-refactor sophistication that engineering-leaning AI gurus respect.

"Chat Profiles, MCP Tools, and a Schema as the Trust Boundary: A Tier-3 Agentic Composer in 71 Commits"
Hooks: name-drops the standards (MCP, schema-first), claims a market tier with receipts (71 commits), no fluff.

"How to Build a Tier-3 Autonomous Analytics Agent Without an Orchestration Framework, a Vector Store, or a Sub-Agent Swarm"
Hooks: contrarian by listing what you don't need — irresistible to AI gurus tired of overbuilt stacks. Sets up your minimalist architecture as the takeaway.


Act 1: Why the V1 Composer Wasn't Good Enough

V1's authoring stepper was a faithful UI mapping of the typed contract underneath. It had to be — that's how the data shape was implemented. AgentDefinitionV1 has a name, a description, a taskSpec, a schedule, an allowedTools array. The stepper just rendered each block of the contract as a form section, validated each step before letting the user proceed, surfaced the linter's 9-check pass/fail.

It was mechanically correct. It was conversationally wrong.

The owner-facing review of V1 surfaced four hard problems:

  • Non-technical users were asked to read lint codes. [tool-availability] means nothing to a sales manager.
  • The dry-run sample report rendered in a separate panel below the typed-field editor — users had to switch context to see what they were authoring.
  • If the LLM proposed something invalid, the user had to fix it by hand in a Zod-shaped form. Most users didn't even know what a draft was.
  • The mental model the stepper imposed (Compose, then Lint, then Dry-Run, then Review, then Publish) didn't match how anyone actually thinks about a report. People think in iteration loops: describe, see, refine, see, refine, publish.
The fundamental insight: the typed contract is the trust boundary; it must stay. But the typed contract should be invisible to the user. The conversation, the inline preview, and the single publish action are the entire surface area. Everything underneath is implementation detail.

Act 2: The Iteration Loop That Reshaped the Plan

I asked Claude to scope the work. The first plan came back at about 600 lines of markdown and proposed a single-PR build — new chat profile, new tools, new UI, all landing together. It described the Composer V2 surface in detail but treated the underlying chat engine as a black box that the new Composer would simply call.

I pushed back twice. The first push was structural: "the AI chat engine bakes a single global system prompt into its runtime. If we want a chat tuned for a different purpose — designing agents, editing forecasts, master-data cleanup — there is no clean way to plug in a different system prompt, a different tool allowlist, a different conversational discipline. That's the real refactor."

That reframing rippled. The plan grew a "Layer 0" — the Chat Profiles platform. Profiles became a first-class abstraction: each profile is a server-defined bundle of { systemPromptBuilder, allowedTools, suggestedPrompts, markerProtocol, hooks? }. The existing global behaviour became the general profile. The new Composer became the composer-v2 profile. The runtime no longer baked one prompt; it looked up a profile at request time.

The second push was sequencing: "ship the foundational refactor first, soak it, then build Composer V2 on top." That split the work into two PRs — PR 1 (foundation, zero behaviour change) and PR 2 (Composer V2 on the soaked foundation). The byte-equivalence safety net got built INTO PR 1 as the "do-no-harm" gate.

Codex did a clean-room review of the v3 plan and posted four findings that materially changed what shipped. I'll paraphrase the gist:

Codex plan-review pass (paraphrased):
  1. "PR 1 needs a byte-equivalence snapshot suite as the do-no-harm gate. Replay a corpus of ~30 canonical prompts against the migrated general profile and assert byte-identical responses against a pre-refactor snapshot. Any drift fails the build. This is the gate, not a nice-to-have."
  2. "Composer-v2 MUST be registered at module-load time, not lazily. If validateChatRequestBody sees an unknown profileId on the first request, every initial Composer invocation 400s. Eager registration with a guardrail test."
  3. "The four authoring tools need a session-scoped allowlist. The Composer agent gets only those four (plus disambiguation tools), nothing else. If a regular chat caller passes profileId: 'composer-v2' by accident, the runtime should still enforce the narrow allowlist server-side."
  4. "Materialise server-owned fields after the LLM returns. id, pk, defType, ownerUserId, systemPrompt, audit, publishToken — never let the LLM author these. Same rule as V1. Pull materializeAgentDefinition out of the V1 Composer into a shared helper so V2 reuses it."

All four landed. The byte-equivalence snapshot suite became the merit gate for PR 1. The eager-registration guardrail is now a permanent test. The four authoring tools live in a hard-coded COMPOSER_AUTHORING_TOOLS array; the bridge filter excludes them from general chat. The materializer was refactored as a pure function and reused by both V1 and V2 Composers.

The final plan landed at 1,771 lines of markdown after about ten review cycles with Codex — almost three times the length of the first draft, because the iterations forced every layer to be specified before any code was written.


Act 3: The Architecture We Landed On

Here's the V2 picture in plain ASCII. Compare to the V1 diagram from the last post — the additions are bracketed by [NEW].

   
   +-----------------------------------------------------------------+
   |                       Web App (browser SPA)                     |
   |                                                                 |
   |   Gallery  -  Compose (V1)  -  [NEW] Compose with AI chat (V2)  |
   |   Filed Reports  -  Insights  -  Admin                          |
   +---------------------------+-------------------------------------+
                               |
                               | HTTPS + auth proxy
                               v
+-----------------------+   /api/agents/*    +-----------------------------+
|  Web App (Express)    |  ----------------> |  AI service (Fastify)       |
|  React shell + static |                    |                             |
|  files. Proxies AI    |   /api/ai-chat     |  +-----------------------+  |
|  routes to the AI     |  ----------------> |  | Chat route            |  |
|  service.             |                    |  | accepts profileId     |  |
|                       |   /api/composer-v2 |  | (default = general)   |  |
|  + serves the new     |  ----------------> |  +----------+------------+  |
|  agent-composer-chat  |                    |             |               |
|  React view + the V2  |                    |             v               |
|  HTTP shims for       |                    |  +-----------------------+  |
|  draft fetch, reset,  |                    |  | [NEW] Chat-Profile    |  |
|  publish.             |                    |  |       Dispatcher      |  |
+-----------------------+                    |  +----------+------------+  |
                                             |             |               |
                                             |       +-----+-----+         |
                                             |       |           |         |
                                             |       v           v         |
                                             |  +---------+ +---------+    |
                                             |  | general | | [NEW]   |    |
                                             |  | profile | | composer|    |
                                             |  | (V1     | | -v2     |    |
                                             |  |  prompt | | profile |    |
                                             |  |  +tools)| | (chat-  |    |
                                             |  +----+----+ |  first  |    |
                                             |       |      |  multi- |    |
                                             |       |      |  turn)  |    |
                                             |       |      +----+----+    |
                                             |       |           |         |
                                             |       |     [NEW] |         |
                                             |       |     four  |         |
                                             |       |     auth. |         |
                                             |       |     tools |         |
                                             |       |  +--------+-----+   |
                                             |       |  | propose      |   |
                                             |       |  | validate     |   |
                                             |       |  | dry_run      |   |
                                             |       |  | materialize  |   |
                                             |       |  | _and_publish |   |
                                             |       |  +--------+-----+   |
                                             |       |           |         |
                                             |       |           v         |
                                             |       |  +-----------------+|
                                             |       |  | [NEW] in-mem    ||
                                             |       |  | draft store     ||
                                             |       |  | TTL 30min,      ||
                                             |       |  | quota 10        ||
                                             |       |  | dry-runs/sess.  ||
                                             |       |  +-----------------+|
                                             |       v           |         |
                                             |  +-----------------v----+   |
                                             |  | Same MCP tool surface|   |
                                             |  | as V1 chat:          |   |
                                             |  |   build_movement_pack|   |
                                             |  |   compute_period_agg |   |
                                             |  |   compute_plan_vs_act|   |
                                             |  |   customer_health    |   |
                                             |  |   query_customer_mas |   |
                                             |  |   get_insight_def    |   |
                                             |  |   search_insights    |   |
                                             |  |   ... (60+)          |   |
                                             |  +----------+-----------+   |
                                             |             |               |
                                             |             v               |
                                             |  +-----------------------+  |
                                             |  | Runtime adapter       |  |
                                             |  | (claude-agent-sdk     |  |
                                             |  |  - unchanged)         |  |
                                             |  +----------+------------+  |
                                             |             |               |
                                             |   Hooks (unchanged):        |
                                             |     PreToolUse  PolicyGuard |
                                             |     PostToolUse Citation+ds |
                                             |     Stop        CostTracker |
                                             |                             |
                                             +-----------+-----------------+
                                                         |
                                                         v
   +-----------------------------------------------------------------+
   |                        Cosmos DB (single account)               |
   |   definitions (incl. agent-definitions from V2 publishes,       |
   |   schema-identical to V1)                                       |
   |   agent_runs (incl. dry-run artefacts owned by composer-v2)     |
   |   bu-ai-data-policy, runtime-state, etc.                        |
   +-----------------------------------------------------------------+

The point worth labouring: everything below the dispatcher is unchanged. The four new authoring tools are MCP tools just like every other tool. The draft store is an in-memory Map with a 30-minute TTL. The runtime adapter, the hooks, the stores, the Cosmos schema, the scheduler — not touched. The Composer V2 surface is purely an additive specialisation.

An agent composed by V2 is byte-identical to an agent composed by V1, up to a single optional audit.composedVia: 'chat' telemetry field. The Gallery doesn't know V2 exists. The scheduler doesn't know V2 exists. If we deleted the V2 surface tomorrow, every previously V2-composed agent would still run.

The non-negotiable invariant: V2 is a better front door, nothing more. It does not fork the agentic engine. Same definition store. Same scheduler. Same runtime. Same hooks. Same ABAC. Same Gallery. Same Filed Reports. Same renderer. Same audit log. The four new authoring tools, the in-memory draft store, and the chat profile are the entire footprint.

Act 4: PR 1 — The Foundational Refactor (Zero Regression)

PR 1 was the first 1.5 days. Six commits. No new user-visible surface. The entire change was that standaloneAiChat.js — the file that owns the chat runtime — learned to look up a chat profile at request time and use it instead of a hard-coded global prompt.

The challenge wasn't writing the dispatcher. The challenge was proving the existing chat behaviour didn't drift by a single byte.

That's what the byte-equivalence snapshot suite was for. ~30 canonical prompts, drawn from a real week of production chat usage (sanitised), replayed against both the pre-refactor runtime and the post-refactor runtime with profileId defaulted to general. The system prompt the runtime composes must be byte-identical. The tool allowlist must be byte-identical. The streaming behaviour must be byte-identical. Any drift fails the build.

The snapshot suite caught one real regression mid-PR — my first attempt at the dispatcher had a sneaky difference in how it normalised the absence of profileId in the request body (treating undefined as 'general' instead of preserving the absence). Codex flagged it during review, the snapshot diffed, and the fix was a four-line change.

Commits in PR 1:

  1. ChatProfileV1 contract in @app/ai-core: Zod schema for { id, label, description, systemPromptBuilder, allowedTools, suggestedPrompts, markerProtocol?, hooks? }.
  2. Profile registry in ai-service: getChatProfile(id), listChatProfiles(), registerChatProfile(profile). Eager registration at module-load.
  3. Inline profileId branch in the chat runtime: look up the profile, call its systemPromptBuilder, filter the MCP tools by profile.allowedTools, apply profile-specific hooks on top of the global ones.
  4. Chat route accepts an optional profileId field. Default general preserves backward compat with every existing caller.
  5. Byte-equivalence snapshot suite — the do-no-harm gate. 30 canonical prompts, byte-identical assertions, fails the build on any drift.
  6. Docs update: agent-contract.md gains a "Chat Profiles platform" section with the platform invariants.

PR 1 sat on the feature branch for a few hours of soak in production before PR 2 began. During the soak the snapshot suite ran on every CI build and never failed. The existing chat experience — AI helper buttons on dashboards, the standalone chat view, page-context-aware tool routing — behaved identically. None of those callers passed profileId; all defaulted to general; none drifted.

That's the value of a do-no-harm gate. A foundational refactor of the chat engine landed on master with confidence high enough that no human had to manually test the existing surface. The snapshot was the proof.


Act 5: PR 2 — The Composer V2 Specialisation

PR 2 was fifteen commits over two days. New profile. Four new tools. New draft store. New frontend view. New HTTP endpoints. New navigation entry. Everything else unchanged.

#CommitWhat it added
1lint translatorPure ai-core helper that translates V1's 9 linter codes into plain English. [tool-availability] becomes "The tool 'X' isn't available for this connector — try 'Y' instead." Engineering codes never leave the server.
2COMPOSER_AUTHORING_TOOLS exclusionThe hard-coded set of four authoring tools. The auto-bridge filter excludes them from the general chat profile so they only surface inside composer-v2.
3Extract materializeAgentDefinitionPure refactor: pulled V1's server-owned-field materialiser out into a shared helper. Reused by V2's propose_agent_draft tool.
4ComposerIntentDraftV1 schemaThe Zod schema for the draft payload the V2 LLM hands to propose_agent_draft. Strict. Field-level error messages. Hand-authored, not derived (Codex caught that a derived schema lost field-level hints).
5Draft store with versioning + publishToken + dry-run quotaIn-memory Map, 30-minute TTL, max 10 dry-runs per session. publishToken is server-issued, never returned to the LLM, only known to the UI's PublishActionChip.
6runAgent accepts AbortSignalLets the V2 frontend cancel an in-flight dry-run when the user backs out. Threaded through the runtime adapter.
7Four Composer tool handlers + definitions + executor wiringpropose_agent_draft, validate_draft_silently, dry_run_draft, materialize_and_publish. Each Zod-validated at the MCP boundary. dry_run_draft calls the existing runtime.runAgent({ trigger: 'dry-run' }) — no V2-specific runtime branch.
8composer-v2 chat profile + EAGER registrationThe profile bundle. Multi-turn system prompt. Suggested prompts. Marker protocol for {{ATTACH_REPORT:runId}}, {{FOLLOWUP:text}}, {{ACTION:publish}}. Registered at module load with a guardrail test.
9Dispatcher branch in standaloneAiChat.jsIf the resolved profile is composer-v2, swap in the COMPOSER_AUTHORING_TOOLS allowlist; otherwise behave exactly as PR 1.
10HTTP endpoints (drafts fetch, session reset, publish)GET /api/composer-v2/drafts/:id for the technical-details disclosure, POST /api/composer-v2/sessions/:id/reset for the "Start a new agent" button, POST /api/composer-v2/drafts/:id/publish for the UI's PublishActionChip.
11SSE tool_result event + AIChatPanel propsStreams structured tool results back to the browser so the chat panel can render the inline report in real time.
12AgentComposerChatView + InlineSampleReport + PublishActionChipThe new chat-first view. Renders the sample report inline using the same AgentReportView sub-components the Filed Reports surface uses — no private copy.
13Navigation entry + AM Contract Performance starter promptSidebar entry "Compose Agent with AI chat" at /agent-composer-chat. AM Contract Performance template surfaces as a suggested prompt.
14MCP bridge coverage test + AM Contract Performance smokeEnd-to-end smoke that drives the Composer through a full session, asserts the published agent is V1-shape, and confirms a spoken name like "City of Cape Town" is REFUSED at propose_agent_draft with a structured hint to call query_customer_master.
15Docs updateagent-contract.md gains the Composer V2 invariants section.

That's the build. Fifteen commits. About 4,300 lines of V2-specific source and 7,200 lines of V2-specific tests. The plan doc was 1,771 lines. The test-to-source ratio is intentionally above 1.5 — the contract is the trust boundary, the tests pin the contract.


Act 6: The User Experience (Currently Lightweight, Evolving)

The V2 surface is deliberately minimal in its first form. ASCII wireframe of the empty state:

+-----------------------------------------------------------------+
|  Compose Agent with AI chat            ?    Start a new agent   |
|-----------------------------------------------------------------|
|                                                                 |
|    Agent Composer                                               |
|    (BU scope . current fiscal year)                             |
|                                                                 |
|    Suggested starters:                                          |
|    [ Weekly Finance briefing ]  [ Monthly performance report ]  |
|    [ Customer health watcher ]  [ Stock coverage review ]       |
|                                                                 |
|                                                                 |
|                                                                 |
|                                                                 |
|                                                                 |
|                                                                 |
|-----------------------------------------------------------------|
|  Converse with AI to craft the agentic report you desire... [>] |
|-----------------------------------------------------------------|
|  > Show technical details                                       |
+-----------------------------------------------------------------+

And the active state, mid-composition:

+-----------------------------------------------------------------+
|  Compose Agent with AI chat                  Start a new agent  |
|-----------------------------------------------------------------|
|                                                                 |
|  [You]    Compose a monthly contract-performance briefing       |
|           agent for City of Cape Town -- sales vs plan,         |
|           delivery progress, debtors, customer health, ...      |
|                                                                 |
|  [Bot]    Got it -- City of Cape Town, monthly, account-        |
|           manager briefing with the seven sections you named.   |
|                                                                 |
|           [O Schedule this report]                              |
|                                                                 |
|           SAMPLE PREVIEW                                        |
|           +-------------------------------------------------+   |
|           | Monthly Contract Performance Briefing --        |   |
|           | City of Cape Town                               |   |
|           |                                                 |   |
|           | Executive Summary                               |   |
|           | Account: City of Cape Town . Municipality .     |   |
|           | Western Cape . Local logistics zone . SA        |   |
|           |   [Sales 01 Aug -- 07 Aug 2025]                 |   |
|           |                                                 |   |
|           | Headline                                        |   |
|           | YTD net sales RXXXX at XXX%    gross margin     |   |
|           | translate to RXXXX of gross profit; the         |   |
|           | account is pacing at YYY% of the RZZZZZ         |   |
|           | annual budget target.                           |   |
|           |   [Sales 01 Aug -- 07 Aug 2025]                 |   |
|           |                                                 |   |
|           |   [chart: SAP Orderbook Insights by Status]     |   |
|           |                                                 |   |
|           | Account Health Composite                        |   |
|           | +-----------+--------+-----------------------+  |   |
|           | | Component | Score  | Signal                |  |   |
|           | +-----------+--------+-----------------------+  |   |
|           | | Debtor    |   50   | All Rabck overdue     |  |   |
|           | | Orderbook |  100   | Ra.bcm vs R282k prior |  |   |
|           | | Delivery  |  n/a   | No delivery signal    |  |   |
|           | | Sales     |   50   | No WoW movement       |  |   |
|           | +-----------+--------+-----------------------+  |   |
|           |                                                 |   |
|           | ... 5 more sections ...                         |   |
|           +-------------------------------------------------+   |
|                                                                 |
|           [ Looks good -- schedule it ]                         |
|           [ Move Risks above Top movers ]                       |
|           [ Add a gross-margin trend chart ]                    |
|           [ Make the tone more detailed ]                       |
|                                                                 |
|-----------------------------------------------------------------|
|  Converse with AI to craft the agentic report you desire... [>] |
|-----------------------------------------------------------------|
|  > Show technical details                                       |
+-----------------------------------------------------------------+

That's the entire user-facing surface in V1 of V2. One chat pane. Inline report preview. Follow-up chips. One disclosed publish button. One collapsed "technical details" disclosure for power users.

The roadmap is to evolve this into a canvas-style dual-pane layout — the conversation in a left rail, the live agent design (sections, charts, schedule, scope) on the right. Drag-to-reorder sections. Inline chart-type swap. Live citation map. The conversation stays the source of truth; the canvas becomes the visual confirmation.

The lightweight chat surface ships first because conversation alone is enough to design a high-quality agent. The canvas is a power-user affordance for refinement, not a precondition for authoring.


Act 7: Six Fix-Cycles, Four QA Rounds

The Composer V2 PR merged on a Thursday afternoon. The one-day production QA loop that followed is what actually convinced me the surface is ready.

Here's the post-merge fix-cycle ledger:

Fix #TriggerWhat landed
1Round-0 production smoke: the "ship it" follow-up chip kicked off the whole workflow again instead of just publishing.Tightened the SHIP rule in the system prompt with explicit "WRONG behaviour at SHIP time" examples. The LLM now responds to "looks good -- schedule it" with a short acknowledgement and STOPS — the orange publish button is the actual publish path.
2Owner direction: non-technical users have no concept of an SAP/simulation data plane; do not surface a "boundary confirmation" chip.Dropped the boundary chip entirely. Boundary acknowledgement is now server-derived. The seven simulation tools are excluded from the agent reporting toolset; plan-vs-actual tools using static targets stay.
3Owner trust incident: agentic surface produced "0 rows" for City of Cape Town across multiple connectors, then auto-rendered top-N tables that included other customers.Stopped auto-generating tableSpecs in composeOutput. Made compute_customer_health_composite honour scope='specific'. Tightened the prompt to ban SAP customer numbers in narrative, to use the correct master-data filter field, and to label BU-aggregate facts explicitly.
4QA round 1: IR-slug ("cust-city-of-cape-town") matched no SAP-keyed row. Every customer-scoped tool returned empty.Built a shared customerScopeResolver module: master-data lookup once, expand each input to the full set of identity strings (customerId, sapCustomerNumber, name, plus slug variants), filter downstream rows by the expanded match-key set. Auto-include query_customer_master + get_customer_deepdive when customerScope is specific. Token budget bumped 512KB → 2MB.
5QA round 2: composer-generated slugs like "cust-city-of-cape-town" still didn't match master rows whose customerId stored a different shape.Slug-to-name reverse transform in the resolver: strip cust- prefix, replace hyphens/underscores with spaces, case-fold, match against row.name. Plus a defensive slug-variant collection so downstream stores keyed by either the friendly name or the slug both match.
6QA round 3: movement pack threw "entityKey must be a function" because the connector was summary-mode (sales/finpack) without the detail-level per-row entityKey.Short-circuit the movement-pack builder for summary-mode connectors with a warning, not a throw. The narrative now reports honestly: "Sales movement pack is summary-mode; per-customer week-over-week diff is on the backlog."
7QA round 3 also surfaced: raw [cite-tool-...] markers were leaking into rendered narrative prose.Defence in depth. Prompt-side: forbid inline citation IDs explicitly. Render-side: stripInlineCiteMarkers() scrubs the narrative before composeOutput calls splitNarrativeIntoSections. Preserves real markdown link refs ([1], [footnote]); only removes cite--prefixed brackets.
8Owner pressure-test: "How confident are you in customer-name lookup for OTHER customers? We only tested City of Cape Town."Option B re-architecture. The resolver becomes strict-equality only (no regex, no slug guessing). The smart fuzzy matching moves UPSTREAM into query_customer_master with three new capabilities: acronym matching (JRA -> Johannesburg Roads Agency, EMM -> eThekwini Metropolitan Municipality), operator-curated aliases harvested from master rows (aliases/searchTerms/shortNames), and a three-status flow (resolved/ambiguous/unresolved) the LLM follows with disambiguation questions.

Each of these landed within hours of being identified. Each was committed to master, pushed through CI/CD, deployed to the AI App Service, and re-tested in production. Then the next fix.

The convergence trajectory: by QA round 4, the inline preview produced a multi-section report with real numbers (Rxxxx YTD revenue, Rxxx budget target, xx% pacing, Rxx outstanding, Rxxxm orderbook surge), a real account-health composite table, citation chips at every section, follow-up chips like "Looks good — schedule it" / "Move Risks above Top movers" / "Add a gross-margin trend chart", and zero raw cite-marker noise. The quality became indistinguishable from the native chat engine's quality on the same prompt.

The convergence rule: ship the surface, then iterate against real workloads until quality matches the existing chat surface on the same prompt. Don't ship without a comparison baseline. Don't accept "the agent gave an answer" as success — the answer has to be as good as the native chat's answer, or you haven't shipped a real upgrade.

Act 8: Closing the QA Loop with Chrome Automation

The thing that compressed the 1-day QA loop into something tractable for one human was Claude driving Chrome directly.

The pattern: I'd describe the issue I'd seen in production. Claude would open the production URL in a real Chrome instance, click through the Composer wizard the same way a user would, type the same prompt I'd typed, wait for the dry-run, screenshot the inline report, compare it to a reference, identify the regression, trace the cause through the codebase, push the fix, monitor CI, verify the deploy, re-run the same flow, and confirm the fix held.

During the last QA round Claude ran ten Chrome sessions in a single afternoon — navigate, reset, send, wait, screenshot, parse, diff, commit, push, wait for CI, repeat. No human intervention except for me reviewing the post-QA summary at the end. One of those cycles ran while I was AFK; I came back to "round 4 complete, here's the rendered preview, no cite markers, narrative quality matches native chat".

This pattern — AI driving the same UI a human would drive, reading its own logs, deploying its own fixes, re-running its own QA — is the single most under-appreciated capability shift of the last six months. It's not a demo. It worked. Repeatedly. In production.

The gating constraints are:

  • The AI must be able to see what the user sees (screenshot, page-text, console messages, network requests).
  • The AI must be able to act on what the user acts on (clicks, types, navigation, form submission).
  • The AI must be able to verify its own work (read backend logs, query the API directly, compare against a known-good baseline).
  • The AI must know when to stop (the loop converges, or hits a hard limit, or hands back to the human).

All four were satisfied in this build. The QA loop ran. The fixes landed. The owner was doing other things or went to bed.


Self-Assessment: Benchmarking Against the Market

I asked one of my non-coding AI co-thinkers (Google's Gemini used purely for market analysis) where this V2 architecture sits relative to the enterprise BI tools shipping in 2026. The assessment was specific enough that I want to capture it here, because it informs what I prioritise next.

Market penetration

Fewer than 5% of production enterprise BI platforms globally provide the exact convergence of capabilities V2 ships: conversational agent authoring + persistent scheduled artefacts + self-healing background automation + deterministic citations + open MCP tool exposure. Conversational BI chatbots that answer questions about existing charts are common (think Looker Studio's Gemini integration, Snowflake Copilot, Power BI Copilot). What's rare is autonomous report-authoring agents that the user composes via chat and that then run unattended on a schedule, with every claim traceable back to an immutable ETL run id.

Tier matrix

TierCapabilityInterfaceWhere it shows up
Tier 1: Conversational BINatural language to query data; ad-hoc charts; single-turn or basic chat thread. No persistence, no background automation.Chat sidebar / input boxLooker Studio (Gemini), Snowflake Copilot, Power BI Copilot
Tier 2: The Data CanvasVisual space; natural-language nodes that auto-generate SQL blocks; chart-organisation on a canvas layout.Unified node/canvas UIBigQuery Data Canvas, Power Apps Agent Studio
Tier 3: Autonomous analytics agentsChat compiles a scheduled employee. Multi-turn refinement, dry runs, self-healing, persistent + scheduled artefacts, citations, MCP-exposed tools.Dual-pane split canvas (conversation + live preview)Anthropic Claude Artifacts, WisdomAI analytics agents, this V2 architecture

Three architectural benchmarks where V2 lands well

1. Agentic compiler UX vs raw text-to-SQL. Most current data tools focus on text-to-code translation. If the LLM misses a comma or references an invalid schema object, the user troubleshoots the raw error. V2 wraps the composition phase in an invisible, asynchronous LLM-to-Zod self-correcting loop. The non-technical user never sees a Zod error. Agent creation is treated like code compilation, with the validation layer hidden behind a directive system prompt and a "fix silently, retry at most 3 times" rule.

2. Open interoperability vs walled gardens. Major cloud database vendors are building closed-loop agent solutions tightly coupled to their proprietary data stacks. V2 anchors execution to the open Model Context Protocol. The Composer can configure agents to run against an isolated ERP database, a local Fastify service, or third-party APIs interchangeably. There's no vendor lock-in in the runtime — the SDK is one adapter file.

3. Absolute traceability. Enterprise AI surfaces frequently generate narrative summaries directly from data snapshots, creating a trust deficit when finance demands auditability. V2 forces deterministic citations: every claim or table row binds back to an immutable ETL run id and dataset id. The LLM never authors a citation; PostToolUse hooks accumulate them onto the run document. The schema rejects any run without coverage. This matches the compliance posture of specialised enterprise analytical platforms.

Benchmark verdict: V2's combination of zero-friction chat-first authoring with rigid, type-safe validation boundaries lands in the "autonomous analytics agents" tier. The next step — the dual-pane split canvas with a live agent-design surface — is on the roadmap and is what closes the visual-confirmation gap relative to the most polished products in the same tier.

Modern Software Development with Two AI Pair-Programmers

I want to spend a section on the actual practice of building this with two AIs and one human, because the pattern is doing real work and I think it's under-described.

The two AIs have different shapes. Claude is the long-context implementer. It holds the full codebase context for hours at a stretch, writes code, runs tests, drives CI, observes production, makes plans, executes plans, reflects, iterates. Its strength is depth and continuity. Its bias is tunnel vision — once it's in a flow, it can miss things adjacent to the work that fresh eyes would catch immediately. It also codes a little hastily, leaving gaps or assuming things without validating deeply.

Codex is the short-context reviewer. It also has long context and memory, but I use it frugally because of the 5-hour time limits, sometimes it enters cold, reads the diff or the file at master, has some prior session memory. Its strength is freshness, depth and architectural rigour — if there's a bug in plain sight, Codex finds it; if a design assumption looks suspect to an outsider, Codex flags it. Its bias is occasional outdatedness and loss of owner's intent — sometimes it's reviewing against an older mental model of the code than master actually carries - or also assumes an intent using its own judgment that needs me to steer it to my design thinking.

The human's job is to steer the two. Claude implements. Codex reviews. Sometimes they both drift and don't get my instinct or intuition for simplifying complexity. The human triages, decides what to take, what to push back on, what to send back to either AI for refinement. The pattern that worked best for V2:

  1. I describe the change I want at a high level. Claude proposes a plan. I push back on framing. Claude revises. Repeat 2-3 times until the plan is the shape I want.
  2. I hand the plan to Codex for clean-room review. Codex returns numbered findings (P0/P1/P2 with rationale). I relay them to Claude.
  3. Claude triages, agrees with most, occasionally pushes back. I adjudicate the disagreements.
  4. Claude implements, commits, pushes, drives CI. I watch the diff over Claude's shoulder. I got Codex to review every single commit from Claude; Codex was the gatekeeper.
  5. I ship to production. I run the surface in Chrome, find issues, describe them.
  6. Claude drives Chrome itself, reproduces, fixes, redeploys, re-tests. Hand back when converged.
  7. I take the merged work back to Codex for a post-implementation review. Codex finds the bugs Claude missed. Claude fixes them. Repeat.

The asymmetry is the leverage. Claude's tunnel vision is Codex's strength. Codex's high-level context is Claude's strength. The human's job is to keep the loop honest — reject sycophantic agreement, demand pushback on weak findings, take credit for nothing the AIs caught.

For V2 specifically: Codex caught five substantive things during plan review and another seven during code review across both PRs. None of them were trivial — the eager-registration guardrail, the byte-equivalence snapshot suite, the hand-written JSON schema for propose_agent_draft.intent, the proxy mount for the V2 endpoints in the root server, the canonical-key terminology audit, the FOLLOW_UPS parser regression tests, the customer-key server-side enforcement. Every one of those would have shipped broken without the second reviewer.

The pattern, distilled: Claude is the depth-first implementer. Codex is the breadth-first reviewer. The human is the architect, the arbiter, and the QA lead. Each AI's bias is the other's strength. Each iteration gets sharper. No one of the three could ship V2 alone.

Metrics

MetricValue
Plan document length1,771 lines of markdown
Total commits during the V2 window (May 12-22, 2026)217
Composer-v2 / chat-profile-specific commits71
PR 1 commits (foundational refactor)6
PR 2 commits (Composer V2 specialisation)15
Post-merge production fix-cycles8
V2-specific source LOC (ai-core chatProfiles + composer/v2 + ai-service composerV2 + customerScopeResolver + frontend view + sub-components)~4,300 lines
V2-specific test LOC (chatProfiles + composer-v2 + customer-scope-resolver tests)~7,200 lines
Test-to-source ratio~1.67
Byte-equivalence snapshot corpus size~30 canonical prompts
Composer V2 tools (authoring + disambiguation)4 + 2
Draft store TTL30 minutes
Dry-run quota per session10
QA rounds against production4 (with ~5 round-0 hotfixes before round 1)
Chrome QA cycles driven by Claude unattended~10 in the final QA round alone
Total elapsed wall time~4 days from "plan-the-V2-refactor" to "round-4-QA-convergence"

Lessons & Advice

For anyone building a similar agentic specialisation on top of an existing AI chat surface, here's what I'd write down up-front.

1. Build the platform abstraction before the specialisation

The strongest decision in the V2 plan was carving out the Chat Profiles platform as Layer 0. Without it, Composer V2 would have been a special case bolted onto the chat engine, and the next specialisation (forecast editor? scenario architect? master-data cleanup?) would have repeated the same bolt-on pattern. With it, future specialisations are a new profile bundle in a registry — no runtime changes, no new endpoints.

The cost: the foundational refactor needed a byte-equivalence safety net (the snapshot suite). The benefit: a one-day soak proved the refactor didn't drift, and PR 2 could land with no fear of regressing the existing chat surface.

2. The do-no-harm gate is non-negotiable for foundational refactors

If you're touching the runtime that powers the existing AI chat experience, you need a mechanical proof that nothing changed. A snapshot of canonical responses, replayed on every CI build, byte-diffed against a pre-refactor baseline. Don't trust manual smoke testing — it misses the prompts you don't think to try. Don't trust unit tests — they pin the parts you remembered to test. The snapshot pins the whole black-box behaviour.

3. Specialise via profile, not via fork

The Composer V2 surface adds a profile, four tools, a draft store, and a UI view. Nothing else. No new runtime branch. No new MCP server. No new hooks. No new scheduler. No new Cosmos partition. No new Gallery. No new renderer. The specialisation is purely additive. An agent composed by V2 is V1-shape downstream; the surface that authored it is a UI affordance, not an architectural divergence.

This is the rule that prevents fragmentation. If your "improved" front door produces artefacts that the rest of the platform doesn't know how to handle, you haven't built a better front door — you've built a parallel platform you now have to maintain.

4. The LLM is authoritative for intent only

The Composer LLM authors a typed payload describing the user's intent: name, description, allowedTools, taskSpec, schedule, customerScope, comparisons, visualPreferences. Everything else — id, pk, defType, ownerUserId, audit, revision, the system prompt itself, the publish token — is server-owned, materialised after extraction, never trusted to the LLM. The schema rejects any draft that tries to set those fields.

This is what prevents the LLM from impersonating users, skipping the runtime's tool-first directive, or short-circuiting the publish gate. The trust boundary is the schema, not the prompt.

5. Build the smart matching upstream, not downstream

The single biggest mistake in V2's first three days was putting fuzzy customer-name matching in the wrong layer — the downstream scope resolver instead of the upstream master-data lookup. The downstream resolver got progressively more regex-laden as edge cases surfaced (slug variants, punctuation, abbreviations). When the owner pressed "how confident are you for OTHER customers, not just City of Cape Town?", I had to admit the resolver was a guess that worked for clean names and failed silently for the rest.

The architecturally correct fix (Option B) was to make the master-data lookup smart (exact + label + alias + acronym + prefix + all-tokens, ranked) and the downstream resolver dumb (strict equality only). The LLM is now expected to call query_customer_master with the user's free-text phrase FIRST, get back a canonical id, and pass THAT to the resolver. No regex anywhere downstream. Acronyms like "JRA" resolve to "Johannesburg Roads Agency" deterministically. Operator-curated aliases on the master row pick up colloquial short-forms like "CCT" without code changes.

6. Hand the AI the browser

If your AI assistant can drive Chrome, your QA loop changes character. The same loop that used to be "human describes bug, AI infers, AI fixes, human verifies, repeat" becomes "AI reproduces, AI fixes, AI verifies, hands back when converged". This is not a luxury feature — it's a compounding capability. Each fix-cycle is hours shorter. Multiple cycles run in parallel. The human supervises rather than drives.

For this to work, the AI needs: clickable accessibility-tree access to the page, the ability to read console messages and network requests, JavaScript-eval in the page context, and screenshot output. All four are table stakes; if your tooling doesn't provide them, prioritise getting them before you optimise anything else.

7. Stop QA when quality matches the comparison baseline

The QA loop's exit criterion was not "the agent gave an answer". It was "the agent's answer is as good as the native chat engine's answer on the same prompt". The four QA rounds existed because that bar was higher than the first three rounds met. Round 4 met it. That was when the loop stopped.

If you don't have a comparison baseline, you don't have a stopping criterion. If your "agentic surface" doesn't have to match the quality of your existing surface, you're shipping a regression dressed as an upgrade.


Best Practices for AI Developers

  • Schemas as the trust boundary. Zod or equivalent. Strict. Frozen before code. Tagged in git. Every interface across an AI surface should be schema-validated; every claim the AI makes should reduce to a schema-conformant artefact.
  • The materialiser pattern. Server-owned fields (id, audit, ownerUserId, system prompt, publish token) get overwritten after the LLM returns. Never let the LLM author identity, audit, or runtime-directive fields.
  • One adapter file imports the SDK. Everything else stays SDK-agnostic. Swap cost is bounded.
  • MCP tools always-loaded; built-ins locked down. If alwaysLoad is false, your model never sees your tools when built-ins are disallowed. This is a known SDK sharp edge.
  • PostToolUse.tool_response is a JSON string. Parse first. Always.
  • Tool-first system prompts. List the tools, then directly tell the model "CALL THESE TOOLS FIRST". The model will not infer the instruction from a list.
  • Citations are tool-handler output. Never LLM-authored. Hooks accumulate them. The LLM writes prose around them.
  • Render via the same components that render the final artefact. Inline previews must use the same sub-components as the eventual filed report — no private copy. Future renderer changes land in one place.
  • Draft stores are in-memory. 30-minute TTL. Per-session quotas. No Cosmos coupling until the user explicitly publishes.
  • Eager registration with a guardrail test. Profiles, plugins, tools — whatever the registration model, prove it at module load and pin the proof with a test.
  • Snapshot suites for foundational refactors. Replay real prompts, byte-diff against a baseline, fail the build on drift.
  • Comparison baseline for QA convergence. Don't ship the new surface without proving its quality matches the surface it's supposed to replace.
  • Two AI pair-programmers, one human supervisor. Long-context implementer + short-context reviewer + arbitrating human. Each AI's bias is the other's strength.
  • Give the AI the browser. Closed-loop QA. Compounding capability.

The Takeaway

The Composer V2 work is what happens when a one-person engineering team treats two frontier AI assistants as pair-programmers and a third AI as a market analyst — with the human as the architect, supervisor, and accountability layer.

The shape of the work is different from solo development. The shape is different from team development. It's not "the AI wrote the code"; it's "the AI implemented the design I steered, while a second AI clean-room reviewed it, while I drove the browser and made the calls about what was good enough to ship." The product of that loop is in production today. Real users compose real agents in plain English; the platform publishes a typed, scheduled, ABAC-scoped, citation-stamped artefact that runs unattended every Monday at 06:30 SAST.

The platform is still small. One person. One repo. One Fastify process for the AI surface. One App Service for the web app. One Cosmos account. No vector store. No background-job platform. No multi-agent orchestration framework. The architecture is deliberately minimalist — the abstractions that matter (schemas, profiles, MCP tools, hooks, structured artefacts) are sharp; the abstractions that don't (a sub-agent swarm, a custom DSL, a parallel data plane) are absent.

What it isn't, is a slide deck. It's a Monday-morning briefing that fires by itself, an Account Manager who composes their own contract-performance agent in a five-minute chat, a citation chip that traces every number back to its SAP source. The Composer V2 surface is one piece of that puzzle — the front door. The next piece — the dual-pane canvas with live agent design — is what closes the gap to the most polished tier-3 products in the market.

Onwards.To. V3 - a fully canvas-style UX that frontier platforms like Claude, ChatGPT and Gemini provide - I'll ship this too, within a week (if time allows). 

Remember I'm just a GM, building my own enterprise BI platform to manage my business. In between my operaional meetings, evenings, and weekends, working with my two AI copilots has unlocked a ton of productivity that is truly amazing!


Stack: TypeScript-flavoured JavaScript end-to-end. Fastify on a Linux App Service for the AI service. Express + React on a separate App Service for the web app. Cosmos DB as the only data store. @anthropic-ai/claude-agent-sdk + @anthropic-ai/sdk for the agentic loop and the chat engine. node-schedule for the cron tick. Recharts for chart rendering. react-markdown + remark-gfm for the markdown renderer. Zod for the frozen contracts. The new abstractions in V2: ChatProfileV1 (Zod schema for chat-engine specialisations), ComposerIntentDraftV1 (the typed draft payload), the four-tool authoring set, the in-memory draft store with TTL + quota, the marker protocol for inline-report attachment, and the byte-equivalence snapshot suite as the do-no-harm gate for the foundational refactor.

Total elapsed: ~4 days from "let's reframe the Composer as a chat" to "round-4 QA convergence in production". Total Composer-V2-specific commits: 71 across two PRs. Total post-merge fix-cycles: 8. Total Chrome QA cycles driven unattended by the implementer AI: ~10 in the final round alone. Cups of coffee: still lost count.