the other half

Things I built because I wanted them to exist.

PANTHEON is the work. This is what the same instincts produce when nobody is paying for the outcome: a real sky on a desktop, an ephemeris with no dependencies, and a terminal that can draw. The discipline is identical; only the stakes change.

GNOME Shell · GPL-2.0 · ★3

TerraFirma: a real sky over your wallpaper.

Not an animation of a sky. The actual one, computed from an ephemeris for your latitude, longitude and clock.

A GNOME desktop whose wallpaper is the computed night sky: constellation
                figures for Pegasus, Aquarius, Aries and Taurus with their names, the Milky
                Way banding across, Saturn labelled and glowing, and the horizon arc marked
                SE and SW. The dock and the Home and Trash icons are in shot.
The desktop, unretouched. The dock and the icons are in shot on purpose: this is the wallpaper, not a window pretending to be one. Every figure, the Milky Way and the horizon are computed for that location and that minute.
Saturn rendered as a small orange disc with its ring ellipse drawn around
                  it, labelled Saturn, against constellation lines.
Saturn at nineteen arcseconds, rings opened and tilted from the real ring geometry. They close to a line every fifteen years; this shows them part-open, because that is where they are.

The hot loop is measurable on purpose

The projection and atmosphere live in skymath.js, which imports nothing from GNOME Shell. A file that can only run inside a session can only be guessed at; one that runs standalone can be timed. That boundary is the same reasoning as the purity lint that keeps PANTHEON's core out of its app layer.

Star catalogue and ephemeris are third-party and credited in NOTICE.md. Point it at a date, then look up, for this kind of software that is the only test that counts.

A wide thin strip of the horizon showing the compass bearing SE marked on
                the horizon arc, with the constellation Piscis Austrinus and its star
                Fomalhaut labelled just above it.
The horizon, with its compass bearings, so you can turn round and check.

Why the hot loop had to move out →  ·  TerraFirma on GitHub ↗

extracted, and published

Three packages that came out of it.

The same pattern as PANTHEON's six: build the thing, find the piece that is genuinely reusable, take it out, and publish it on its own terms. All three are MIT and installable right now.

The Mona Lisa rendered in a terminal as coloured Unicode braille
                  characters. Each character cell holds a 2x4 grid of dots; the face, hands
                  and the hazy landscape behind her are all recognisable, and the braille
                  dot texture is visible inside every cell.
The Mona Lisa, drawn with Unicode braille in a terminal, 93 columns wide. Every cell is a 2×4 dot grid with its own foreground and background colour.

braillecanvas v0.4.0 · MIT

Braille addresses 2×4 dots per character cell, so an 80×24 terminal is really a 160×96 canvas. That is the difference between a rendering and ASCII art.

The interesting part is colour. Each cell carries two colours, one for the lit dots, one for the gaps, so a cell straddling an edge, skin against sky, keeps the edge instead of averaging it into mud. Mean per-dot colour error on this image falls from 45.7 to 21.3.

Choosing which dots light up per cell, rather than taking the pattern from a fixed dither grid, cuts textureless cells from 35.1% to 1.6%.

Two limits stated in its own README: below about 90 columns photographs stop working, and monochrome cannot render this picture at all, her face measures 159 luminance against 192 for the sky behind her, so a 1-bit render is a silhouette by construction.

npm i braillecanvas · npm ↗ · source ↗

skymaths v0.1.2 · MIT

Positional astronomy with no dependencies, Sun, Moon, planets, twilight, rise and set. Extracted from TerraFirma's ephemeris, then corrected: the Espenak–Meeus polynomial it used is published for 1900–1920 only and was being applied to 1986, giving −534 seconds for 1950 against a measured +29. Found by checking it against a published almanac rather than against itself.

It is running on this site: in dark mode the background is the sky over Cardiff on Perseids night, 13 August 2026, computed in your browser: 5,044 stars in their real colours, the Moon's phase and the planets where they were. Scroll to turn round.

npm i skymaths · npm ↗ · source ↗

starwheel v0.2.3 · MIT · ★4

A live planisphere in your terminal, real star positions for your location and minute, drawn on braillecanvas. My most-starred repository, and the one that taught me a fix which moves published pixels is not the same fix twice: it bounds iterations where its sibling clips, deliberately.

npx starwheel · npm ↗ · source ↗

A terminal server monitor drawn in braille: CPU, memory and load-average
                line charts over a rolling window, with a per-core bar strip.
The same canvas plotting real /proc data, CPU, memory and load average over a rolling window. Charts work far below the 90-column floor that photographs need, because they are already high-contrast.

on this page, right now

The art is generated, and the seed cannot escape.

Every piece of art on this site: the hero above, the favicon, the social cards, the avatars, comes out of PANTHEON's own generator. Deterministic and seeded: the same seed gives the same image, every time.

Same contract on both sides

A server-side engine renders the social cards and tenant sites; a browser engine draws the animated version you are looking at. Same rule: seed in, reproducible art out.

Injection-safe by construction

The seed feeds the PRNG and nothing else. it is never interpolated into markup or a style. A tenant name that is really a payload draws a different picture; it does not draw a script tag. The same instinct as the rest of the system, applied to decoration.

installable, not just readable

Six guarantees, extracted and published.

Each one came out of PANTHEON as a single verifiable promise, then went to PyPI so it stands on its own. Adversarial AI agents, self-run, not independent, went through them line by line and found real defects; the versions below are the ones after those fixes, each verified by installing from the registry into a clean environment and re-running the reviewer's own reproduction, not by trusting the build log.

pantheon-guardrails v0.3.2 · Apache-2.0

A constitution scorer that spends an LLM judge only on high-stakes replies, and requires that judge to be a different model from the generator so the two don't share blind spots. It used to clamp an out-of-range score upward: a judge returning {"clarity": 99} scored a perfect 1.0 and passed every threshold: a guardrail failing open. Now a judge that raises, hangs or returns nonsense is an explicit failed evaluation that blocks by default.

pip install pantheon-guardrails · PyPI ↗ · source ↗

pantheon-ssrf-guard v0.2.1 · Apache-2.0

A two-layer SSRF egress guard that survives DNS rebinding: it re-checks the IP the socket actually reached at connect time, so a host that resolves public then rebinds to loopback or cloud metadata is refused. The review found the opener would still read file:// URLs, because Python installs handlers for other schemes by default; there is now an explicit http/https allowlist at the boundary, redirects included, and environment proxies are off unless you ask for them.

pip install pantheon-ssrf-guard · PyPI ↗ · source ↗

pantheon-tool-sanitizer v0.3.0 · Apache-2.0

Strips tool-protocol markup and Unicode smuggling from untrusted MCP tool text before it reaches an agent's prompt. Honestly scoped: it does not claim to stop plain-prose injection, which is an architecture problem rather than a string one. Two fixes from the review: a 256 kB adversarial input took 26 seconds of CPU and is now refused outright, and "Read\nthe\tfile" no longer becomes "Readthefile", because a line break is a word boundary.

pip install pantheon-tool-sanitizer · PyPI ↗ · source ↗

credit-ledger v0.3.0 · Apache-2.0

Overdraft-proof metering over Postgres in about 150 lines: 100 parallel charges against a balance of 50 and exactly 50 succeed, exactly-once billing under webhook replays, isolation enforced by row-level security. The published build used to accept a negative charge, which paid the customer, and amounts below the column's precision, which billed 0.0000 while reporting success. One amount rule now covers every path, and there is a tested migration for databases that already exist.

pip install credit-ledger · PyPI ↗ · source ↗

pantheon-ical v0.3.1 · Apache-2.0

Round-trip iCal (RFC 5545) for booking calendars in under 200 lines. A dropped busy period reads as free, so silence is the dangerous failure: an oversized calendar is refused rather than truncated, and a recurring rule that overflows says so instead of returning a short list. The last fix bounded the work as well as the result: a one-line hostile rule used to build 34 million occurrences over 76 seconds before being rejected; it now stops at 401.

pip install pantheon-ical · PyPI ↗ · source ↗

pantheon-rls v0.1.1 · Apache-2.0

Tenant isolation as a Postgres guarantee rather than an application habit: force-RLS plus least-privilege grants, so the database itself refuses cross-tenant reads and writes. Fail-closed by construction, no tenant context returns zero rows, never all rows, which is the failure mode that matters when a bug reaches production at 3am.

pip install pantheon-rls · PyPI ↗ · source ↗

Every fix above arrived the same way: reproduce the defect first, fix the boundary rather than the symptom, then prove the new test fails when the bug is put back. A test that cannot fail is not evidence.

what it runs on

The stack is boring. The boundary is not.

Python 3.12, FastAPI, Postgres, Redis, React with Vite, one Dockerfile behind nginx. None of that is a decision worth admiring, those are the parts you pick so your attention can go somewhere that matters.

146 files · 14,002 lines

pantheon-core: the substrate. Domain-agnostic by contract: it may not import from residents or bundles, and two independent checks fail CI when it does.

210 files · 24,216 lines

Tests, against 38,816 lines of Python. I don't hand-write the implementation, so the suite is not a safety net. It is the specification, and it is the artefact I actually author.

Five layers, one way

architect · runtime · guardrails · substrate · primitives, with providers deliberately outside the stack, because it sits below two layers at once and the contract says so out loud rather than bending.

That boundary is why six components could be lifted out and published as standalone libraries, extraction was mostly moving files that had never been allowed to reach downward. The long version, including the part I got wrong →

Same person, same rules, lower stakes.

Nobody audits a wallpaper. The reason these are built the way they are is that it is the only way I know how to build, measure it, extract the reusable part, publish the limits with it, and check the numbers against something outside the program.

What I'm looking for → Read the writing →