PAI: A conversational supplement to the traditional portfolio

PAI is personal AI — pronounced pie. The chat assistant on this site — a conversational layer alongside the portfolio, not a replacement for it.

01 — What it is

A portfolio that answers instead of presenting.

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 side panel rather than describing it, and answers at the depth the question asked for. The window above is not a screenshot or a prototype — it 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 routes instead of presenting, and it does it without leaving the visitor guessing about what it knows.

Agents wrote a lot of this code. None of it shipped without me reading it.

02 — Stack and ownership

One owner from the retrieval query to the pixel.

FRONT ENDReact · Vite SPA · TypeScript · hand-written CSS, no UI framework
BACKENDNode · Express · TypeScript · SSE streaming · SQLite
MODEL LAYERClaude (primary) · OpenAI fallback · tool calling · keyword retrieval
SURFACEWordPress block theme (PHP, JS, CSS) · Apache reverse proxy · PM2 on Lightsail
I OWNProduct · design · front-end code · the review gate on everything an agent wrote

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, that was my CSS and my fix. When the streaming route dropped an error chunk silently and visitors saw nothing on a model failure, that was my bug, my instrumentation, and my 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

Speed is cheap. Being wrong in production is expensive.

I run coding agents in three modes, and the mode is a decision I make before the work starts — not something I discover afterward.

Pairing, in the moment

For interface work where taste is the constraint — spacing, hierarchy, motion, the last ten percent — I stay in the loop turn by turn. The agent is faster at typing than I am; it is not better at knowing 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 not a vibe — it 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 case study now has a verifier wired into the deploy path, and the deploy exits non-zero rather than shipping it again.

That is the honest version of accountability for output I did not personally type. I can tell you what every patch does, why it exists, and which check would catch it if it regressed.

04 — Acceptance criteria

Write the failing case before you 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 the only way that counts. 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 a portfolio assistant that invents an email address is worse than no assistant.

05 — The design decision I keep defending

Where trust matters, don’t ask the model — intercept.

The tempting architecture is to let the model handle everything and tune the prompt when it misbehaves. That works until the path has consequences. Requesting access to gated case studies, 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 exist for the same behaviors. Both layers stay. The prompt covers the long tail; the intercept guarantees the cases where a miss costs 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

The proof is that 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 normalization pipeline tuned for pace and pause rather than raw text-to-speech.

Open questions 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 the build notes.