01: What it is
A portfolio you can ask.
PAI is the chat interface to this studio site, running in production at /chat/. A visitor asks a question in plain language; the system retrieves grounded material from a curated corpus, opens the actual portfolio page in a reading pane rather than describing it, and answers at the depth the question asked for. The window above is the same service the rest of the site uses.
A static portfolio shows every visitor the same pages in the same order. A hiring manager evaluating fit, a practitioner checking method, and a collaborator looking for a specific decision all need different paths through the same material. PAI takes each of them to the right pages and says what it is drawing on.
Agents wrote a lot of this code. I read all of it before it shipped.
02: Stack and ownership
One person owns it, from the retrieval query to the screen.
There is no engineering team behind this and no handoff. I write the product intent, design the interface, implement the front end, review the agent-written diffs, and run the deploy. When the panel layout broke on iOS Safari, I fixed the CSS. When the streaming route dropped an error chunk and visitors saw an empty bubble on a model failure, I found the bug, added the logging and wrote the patch.
The same ownership runs through the site around the chat. The theme is a custom WordPress block theme: the case-study layouts, the password gate, the panel-embed stylesheet that strips site chrome inside the iframe. Content ships through a Python publish pipeline I wrote against the WordPress REST API rather than through the admin editor, so the repository stays the source of truth.
03: Directing the agents
Agents are fast. Production is where being wrong costs something.
I run coding agents in three modes, and I choose the mode before the work starts.
Pairing, in the moment
For interface work, where the hard part is judgement (spacing, hierarchy, motion, the finishing), I stay in the loop turn by turn. The agent types faster than I do. It does not know when a thing is finished.
Scoped, then reviewed
For well-bounded changes I hand over a written scope and read the diff before it moves. The output lands as a named patch script in the repository, so what shipped is legible six weeks later and re-runnable against a rebuilt environment.
Never unattended into production
Every change lands on staging first, under my own QA, and production is a separate explicit step. The rule is written into the repository instructions the agents read, so the constraint holds even when I am moving fast.
The review gate is mechanical. Roughly seventy-five server patch scripts live in the repository, each one a reviewed unit of change with its own runbook, rather than a pile of undifferentiated agent edits on a server. The deploy script refuses to run when a structural check fails: a regression that reintroduced a double password prompt on the way from a bridge page to a gated write-up now has a verifier wired into the deploy path, and the deploy exits non-zero rather than shipping it again.
For code I did not type, that is what accountability looks like: I can say what every patch does, why it exists, and which check would catch it if it regressed.
04: Acceptance criteria
I write the failing case before I trust the fix.
The clearest example is model failure. PAI’s answer path depends on a third-party model, and the first version of the failure path was silent: the front end dropped the error chunk and the visitor watched an empty message bubble. I wrote the acceptance criteria before the fix: on a provider failure before any tokens stream, fall back to a second provider and mark the turn in the response metadata; on total failure, stream a plain-language apology with a real contact address rather than nothing; never splice a second provider into a half-streamed answer.
Then I verified it on staging. On staging I invalidated the primary API key and confirmed the fallback flag appeared in the stream metadata, then stripped the second key as well and confirmed the graceful message rendered end to end. Both conditions had to pass before the change went anywhere near production.
The same discipline covers behavior that no unit test reaches. I run scripted persona sessions: a recruiter, a skeptical engineer, a practitioner asking about method, and catalogue where the system answers confidently and wrongly. Most of the prompt rules in the system trace back to a specific logged failure: the employment-status fact is force-included on every turn because a simulation caught the model asserting a stale employer when retrieval happened to miss it. There is also a standing adversarial probe suite that runs against production for prompt injection, gate-bypass attempts, tool coercion, and fabricated contact details, because an assistant that invents an email address does real damage.
05: The decision I keep defending
Where a miss costs a real opportunity, the server decides, and the model narrates.
The easy architecture lets the model handle everything and tunes the prompt when it misbehaves. That holds until a path has consequences. Requesting access to gated write-ups, surfacing a contact route, and a handful of high-intent starter prompts are handled deterministically in the route layer: the server pre-emits the correct panel event, clears the stale panel state, and rewrites the turn so the model narrates something that is already true on screen.
Prompt rules still cover the same behaviours, so both layers stay: the prompt handles the long tail and the intercept handles the cases where a miss would cost a real opportunity. The trade-off is that these paths are less flexible than the rest of the product, and I would make that trade again: a visitor asking how to see the full work should never receive a paragraph explaining that they cannot.
06: What’s live and what isn’t settled
It is live, and you can use it.
PAI is in production and has been since early 2026, taking real visitor questions with a per-response feedback control feeding an admin telemetry layer. The panel opens real pages through a same-origin proxy, tracks which sections are in view, and scrolls to the section being discussed as the answer streams. Voice runs through a streaming speech-to-text proxy and a normalisation step that shapes pace and pauses before text-to-speech.
Things I have not solved: the output cap is a single fixed number when it should tier on explicit depth cues; conversation storage grows without a retention policy; the opening greeting is static because I judged first-token latency more valuable than personalization, and I have not revisited that since. Generated panels are implemented and underused, which probably means the trigger conditions I wrote are too narrow.
Ask the assistant above about any of it. It has read these notes.