Job sprint · defend the work live

Interview Walkthrough Trainer

Six systems plus the hard questions, drilled until you can whiteboard them from memory. Every claim is verified against your actual code — including the gaps, so you own them before an interviewer finds them. One 60-minute session a day, out loud.

Today's session

One 60-minute session a day, mixed across all six projects plus the hard questions. Scheduled cards come first ("Again" repeats today; "Good" moves them out 1→3→7→14 days; "Easy" jumps two boxes). When the due queue empties before the hour, the session flows into practice reps — pitches, whiteboards, and your weakest cards — until 60:00. Practice grades don't inflate the schedule; "Again" still honestly resets a card.
0cards due now
0day streak
0/0mastered (box 4+)
The rule that makes this work: say your answer out loud, in full sentences before revealing — retrieval is the workout, reading is not. Grade honestly: "Good" means you'd have said it in an interview; hesitation or half-answers are "Again". Once a day, also do one Blind-mode whiteboard from a project tab.

Sync

Manual backup — copy progress by hand (works offline)

Press Copy my progress here, open the trainer on the other device, press Import a code and paste. The code carries boxes, due dates and streak.

Support Automation Control Center

Your flagship. n8n → Next.js/Prisma → Claude/GLM → human approval → Resend. Repo: support-automation-control-center.

60-second pitch — for “tell me about something you’ve built”

This is a human-in-the-loop support system: AI drafts every customer reply, but a human approves every send — the AI physically cannot email a customer, because the send function only exists inside the approval route.

Tickets arrive through an n8n webhook into a Next.js and Prisma app. The app retrieves the relevant support policies, then Claude — or GLM, it’s provider-routed — classifies the ticket and drafts a policy-grounded reply. I don’t trust the model: its JSON is validated with Zod, and then a deterministic keyword layer can override its classification upward, so an angry legal-threat ticket can never be silently downgraded to low priority.

A safety scan runs on the draft; a reviewer edits it in the dashboard; and at approval the safety check runs again on the final human-edited text before Resend delivers it. Every step writes an append-only audit event, with cost, latency and token telemetry recorded per AI call.

0:00

The whiteboard — draw it in this exact order while talking

Blind mode. Diagram and steps are hidden. Draw the full pipeline on paper from memory, out loud, start the 5:00 timer. Reveal when done and compare against every box and arrow.
flowchart LR
  A["n8n webhook
+ shared secret"] --> B["POST /api/tickets/ingest
Zod validate"] B --> C[("Ticket row
+ audit #1")] C --> D["POST /:id/draft
policy retrieval"] D --> E{"Provider router
Claude / GLM"} E --> F["Trust layer
Zod + keyword override"] F --> G["Safety scan #1"] G --> H["Dashboard
human edits"] H --> I["approve: passcode
+ safety scan #2"] I --> J["Resend send"] C -.-> K[("AuditEvent
append-only + telemetry")] J -.-> K
  1. n8n webhook — intake from email or any channel; normalizes payload, attaches a shared secret header.
  2. Ingest route — verifies the secret with a timing-safe compare, Zod-validates the body, writes the Ticket row and the first audit event.
  3. Draft route — retrieves top-3 policies by keyword scoring over a 5-policy JSON file (say “keyword-scored, not vector RAG — at five policies, vectors are overkill”).
  4. Provider router — Claude Sonnet or GLM by env var, temp 0.2, JSON-only prompt, deterministic fallback draft if anything fails.
  5. Trust layer — extract JSON from the response, Zod-validate the schema, then a deterministic keyword pass that can only upgrade intent/priority, never downgrade.
  6. Safety scan #1 — regex tripwire; severity decides drafted vs needs_review.
  7. Dashboard — human reviews and edits; every edit is an audit event.
  8. Approve route — passcode (timing-safe), then safety runs again on the final human-edited body. Flag → 409, back to review.
  9. Resend delivery — live only when explicitly enabled, else simulated; result and failures audited.
  10. AuditEvent table beside everything — append-only, holds per-call cost/latency/tokens in metadata.
0:00

Drill cards — tap to reveal, say the answer out loud first

Why an approval gate instead of just sending?

Liability and trust: this sends to real customers. It’s structural, not policy — the send function is called exclusively inside the approve route, so there is no code path where AI output reaches a customer unreviewed. A human plus a passcode is physically required.

tap to reveal

What happens when the LLM returns garbage?

Three layers. The extractor tolerates code fences and surrounding prose; if JSON parsing or the Zod schema fails, the catch returns a deterministic fallback draft with the route reason recorded — so nothing crashes, and the reviewer always has something to work with. Same path covers missing API keys and provider errors: the app runs with zero credentials.

tap to reveal

Why does the safety check run twice?

Because the human is also a risk. The first scan checks the AI draft; the second runs server-side at approval on the final, human-edited body — a reviewer can introduce unsafe language while editing. A flag at approval returns a 409, writes a safety_flagged audit event, and forces the ticket back to review. That’s genuine defense-in-depth and my favourite detail in the system.

tap to reveal

Why override the model’s classification with keywords?

The model is a suggestion, not an authority, for routing. Deterministic keyword floors can only upgrade — a ticket containing a legal threat can’t be silently classified low-priority by a bad model day. I’ll also volunteer the tradeoff: keyword lists are a maintenance burden and risk overfitting to my eval fixtures — at scale I’d move that to a second cheap classifier plus sampling.

tap to reveal

How do you know what each reply cost?

Every AI call is stamped with provider, model, latency, token counts and estimated cost in USD, stored in the audit event’s metadata. So cost and latency are queryable per ticket without an external APM — you can answer “what did this customer interaction cost us” from the database.

tap to reveal

Where does it break under retry or load? (They will ask.)

Two honest failure points I’d fix first in production. No idempotency: a retried n8n webhook creates a duplicate ticket — I log the execution ID but don’t dedupe on it yet. And approve has no already-sent guard, so a double-click could double-send. The LLM call also runs inside the request handler, blocking the response. The fix is one package: dedupe on execution ID, a status guard on approve, and moving drafting onto a job queue with retries.

tap to reveal

Is the policy retrieval RAG?

No, and I won’t call it that. It’s keyword scoring over a five-policy JSON file, with the cited policy IDs recorded so every draft shows which policy drove it. At five policies, embeddings are overkill; at five hundred I’d move to vector retrieval — the retrieval function is already isolated so that swap is contained.

tap to reveal

What’s the testing story?

Honest answer: an 11-case deterministic eval harness — six classification, five safety — that runs on the fallback rules path, not a unit test suite, and it never exercises live model output or the API routes. I’d call it an eval harness, not tests. My booking center is the opposite: 49 Vitest tests including integration tests against real Postgres. First thing I’d add here is route-level tests and evals against recorded live outputs.

tap to reveal

What’s the auth story?

Single-tenant by design: one shared approval passcode, compared timing-safe — and I’ll volunteer that it has an insecure default if the env var is unset, which I’d remove. Reviewer identity is hardcoded, so there’s no per-reviewer attribution. First production change: real user auth, per-reviewer identity in the audit trail, and RBAC on approve.

tap to reveal

Why only two database tables?

Deliberate at this scale: a Ticket that carries its evolving state, and an append-only AuditEvent — everything else, policies and safety config, is code and JSON. State transitions live in the audit trail rather than a dozen status tables. The moment this went multi-tenant or added reviewers, I’d break out users, orgs and policies into real tables.

tap to reveal

Own it before they find it

  • No queue, no idempotency. Duplicate webhooks make duplicate tickets; approve can double-send; the LLM call blocks the request. Fix: dedupe key + status guard + job queue.
  • Thin auth. Shared passcode with an insecure default; hardcoded reviewer identity; single-tenant.
  • Eval harness, not a test suite. 11 deterministic cases on the fallback path only. Zero route/component tests.
  • Safety is regex. A tripwire, not a guarantee — paraphrase walks past it, and only high severity blocks. I’d hybridize rules + a cheap model check.
  • PII in plaintext. Names, emails, message bodies sit unencrypted in Postgres and audit JSON. Fine for a demo, not for production.
Never claim:
  • “RAG” or vector retrieval — it’s keyword scoring over 5 policies.
  • “Test suite” — it’s an 11-case eval harness (and the README says 10; the real number is 11, endpoints are 7 not 6 — correct it before they do).
  • A development story from this repo’s git history — it’s one squashed release commit. Point to the booking center for real history.

Numbers to know cold

~3,200lines of TypeScript
7API endpoints
2Prisma models (say it plainly)
11eval cases (6 classify + 5 safety)
5external services
safety scans per message

Stack: Next.js 16 · React 19 · Prisma 6 · Zod 4 · Supabase Postgres · Claude Sonnet / GLM · Resend · n8n

AI Lead Response & Booking Center

Your velocity + rigour proof: 68 commits in 3 days, 49 tests. Repo: ai-lead-response-booking-center.

60-second pitch

This is a lead-intake and booking system for service businesses, and it’s my velocity-with-rigour proof: sixty-eight commits over three days, with forty-nine tests.

Every lead — web form, dashboard, or raw API — funnels through one Zod-validated endpoint. The bare lead is persisted first, so if AI fails I’ve still captured the customer. Then a pluggable provider extracts and scores it: GPT-4o-mini when a key is present — and here’s the part I like — its output is validated against the exact same Zod schema my deterministic rule-based provider satisfies. So garbage model output fails validation and transparently degrades to rules, with the engine recorded on every row, and there’s an integration test proving that end to end.

Extraction, lead update and follow-up draft commit in one transaction. A human approves every message, bookings are transactional with double-booking blocked by a database unique constraint, and every step lands in an automation log plus a CRM pipeline-event history.

0:00

The whiteboard

Blind mode. Draw the intake pipeline and the booking flow from memory, out loud, 5:00 on the clock. Then reveal and compare.
flowchart LR
  A["3 doors: form,
dashboard, API"] --> B["one endpoint
Zod validate"] B --> C[("bare Lead saved
before AI")] C --> D{"Provider factory
gpt-4o-mini / rules"} D --> E["same-schema Zod
check on LLM JSON"] E -->|fail| F["rule-based fallback
engine recorded"] E --> G["merge contacts
form beats AI"] G --> H[["one $transaction:
extraction + lead + draft"]] H --> I["human approves
draft — idempotent"] I --> J["book slot
unique constraint"] H -.-> K[("AutomationLog +
PipelineEvent")]
  1. Three doors, one pipe — public contact form, dashboard quick-add, and raw POST all hit the same endpoint. No separate demo path.
  2. Zod at the boundary — invalid input is a 400 with flattened errors before anything touches the database.
  3. Persist before AI — the bare lead and a lead_received log are written first, deliberately outside the transaction: if the model dies, the customer isn’t lost.
  4. Provider factory — OpenAI GPT-4o-mini if a key exists, else a deterministic rule-based scorer (base 20, +email, +phone, +service, +urgency, capped at 100).
  5. Same-schema validation — the LLM’s JSON must pass the identical Zod schema the rule-based provider satisfies; failure falls back to rules, and the engine used is recorded on the row.
  6. Contact coalescing — explicit form fields beat AI-extracted ones; safety flags recompute on the merged identity.
  7. One transaction — extraction + lead update + follow-up draft: all land or none.
  8. Human approval — draft flips to approved, notification staged; idempotent, so double-click can’t double-stage.
  9. Booking — in-transaction open-slot check, but the real guarantee is the database: slot ID is unique on Appointment, start time unique on slots. DST-safe slot generation in the business’s timezone with date-fns-tz.
  10. Dual audit — AutomationLog for the per-lead activity feed, PipelineEvent for every CRM stage move.
0:00

Drill cards

Why persist the lead before running AI?

Because the customer matters more than the enrichment. If OpenAI is down or returns garbage, I still have their name in the funnel and a lead_received log row. It’s deliberately outside the later transaction — an early write that survives any downstream failure.

tap to reveal

Walk me through the fallback design.

One interface, two providers. The clever bit: the LLM’s JSON output is validated by the exact same Zod schema the rule-based provider is built to satisfy. So a malformed response fails the parse, the catch swaps in the deterministic extractor, and the row records which engine produced it. There’s a unit test feeding it lead_score “not-a-number” and an integration test proving the whole degradation end to end. It also means the app runs with zero AI budget.

tap to reveal

How do you prevent double-booking — really?

Two layers, and I’ll be precise about which one is the guarantee. In the transaction I check the slot is still open — but under true concurrency that check is racy. The actual guarantee is the schema: slot ID is unique on Appointment and start time is unique on slots, so the loser of a race gets a constraint violation. Honest gap: that loser sees a raw Prisma error rather than a friendly “slot taken” message, and booking isn’t idempotent — both on my fix list.

tap to reveal

What was the hardest bug?

Timezones and DST in slot generation. “Today, 9 to 5” has to mean the business’s wall clock, not the server’s — so slots are generated with date-fns-tz, resolving today in the business timezone and converting properly across DST transitions. And there’s a documented performance decision in the code: batched createMany with skipDuplicates keyed on the unique start time, instead of an upsert loop that took 24 to 50 seconds against the Supabase pooler.

tap to reveal

Does it actually send the SMS/email?

No — and I say that before anyone asks. Notifications are staged, never dispatched; the status enum literally has one value. The system is AI drafting plus human approval plus staged messages; Twilio and Resend are the designed next step. I built the send leg in my support desk project, so wiring it is understood work, not unknown work.

tap to reveal

What would a senior engineer criticize?

The CRM state machine isn’t enforced — any stage can move to any stage, so a lead can reach “booked” with no appointment row. The intake endpoint is public with no rate limiting. There’s no auth — the actor is literally “dashboard user”. And my integration tests run against a real shared database, so they can’t run in CI. I know the fixes for all four; they’re scoped, not mysterious.

tap to reveal

68 commits in 3 days — how, honestly?

Claude Code as the pair, me as the architect and reviewer. Small vertical slices, each commit a working increment, tests written alongside — that’s why 49 of them exist. The AI writes fast; the discipline — what to build, what to validate, what to test, what to refuse to ship — is mine. I’m happy to open any commit and explain it.

tap to reveal

Why one endpoint and server actions instead of a REST API?

One write path per concern. Intake is the single API route because external systems post to it; dashboard mutations are three server actions — approve, book, move stage — so business rules live server-side next to the transaction, not scattered across fetch handlers. Ten Prisma models underneath: Lead, LeadExtraction, FollowUpDraft, AvailabilitySlot, Appointment, PipelineEvent, Notification, AutomationLog, plus Business and BusinessHours.

tap to reveal

Own it before they find it

  • State machine unenforced. Any CRM stage → any stage; “booked” possible with zero appointments. Fix: a transition map validated in the server action.
  • Race handled by constraint, not code. Correct outcome, ugly error for the loser; booking not idempotent.
  • No auth. Hardcoded “dashboard user”; business via findFirstOrThrow. Single-tenant demo posture.
  • Messages staged, never sent. Say it first; point to the support desk’s Resend leg as the proven pattern.
  • Tests not hermetic. Integration suite needs a real database; CI only typechecks and lints.
Never claim:
  • “It sends messages” — staged only.
  • “Business rules can’t be bypassed” — the write path is centralized; the rules aren’t enforced yet.
  • “Fully transactional double-booking prevention” — the unique constraint is the real guard; say so, it sounds better anyway.

Numbers to know cold

68commits · Jul 4–6 (24/34/10)
49Vitest tests · 18 files
~3,420app LOC + ~950 test LOC
10Prisma models
1 + 3API route + server actions
2external services

Stack: Next.js 15 · TypeScript · Prisma 5 · Supabase Postgres · GPT-4o-mini · date-fns-tz · Vitest

AI Ops Agent

Your MCP story — the hottest interview topic of 2026. Python FastMCP, 24 tools, SQLite + markdown memory. Repo: ai-ops-agent.

60-second pitch

This is a self-hosted operations agent, and it’s my MCP story. A Python FastMCP server exposes twenty-four typed tools over stdio to any MCP-capable model. The design principle: the model decides, but the tools own all state and safety — every tool returns a structured ok-or-error result, never an exception, so the agent loop can’t crash on bad input.

Memory is deliberately split: SQLite for structured state — tasks, moods, activity — and an Obsidian-compatible markdown vault for narrative memory. Semantic search stores OpenAI embeddings as float-32 blobs in SQLite and computes cosine similarity in pure Python — full local retrieval with zero vector-database infrastructure.

My favourite piece is bidirectional task sync: every task renders into a markdown file with a block-reference ID, and a ten-minute cron parses ticked checkboxes back into the database. Tick a box in your notes app, and the task closes in SQLite. Cron handles indexing, sweeps and a daily digest; a FastAPI dashboard watches it all, read-only.

0:00

The whiteboard

Blind mode. Draw the four planes — MCP server, memory, cron, dashboard — and the task round-trip from memory, out loud. 5:00.
flowchart LR
  M["Any MCP client
model runtime"] <-->|stdio| S["FastMCP server
24 typed tools"] S --> DB[("SQLite
structured state")] S --> V["Markdown vault
narrative memory"] S --> X["Providers via urllib
OpenAI / Exa / Firecrawl"] DB --> T["tasks.md render
with block-ref IDs"] T -->|tick a box| C["Cron plane:
index 15m, sweep 10m,
digest 21:30"] C --> DB C --> V D["FastAPI dashboard
read-only"] -.-> DB D -.-> C
  1. MCP client ⇄ server — the runtime spawns the Python server over stdio; FastMCP derives each tool’s JSON schema from type hints and docstrings, and validates every call.
  2. 24 tools in 6 groups — vault & semantic memory (4), tasks (4), daily state & activity (6), calendar (3), multimodal (4), data & web (3).
  3. The contract — every tool returns ok-true-or-false with data or error; failures are returned, never raised, so the loop survives anything.
  4. Split memory — SQLite for queryable state, markdown vault for narrative; single-file database means zero ops and backup-by-copy.
  5. Path safety — the model picks file paths, so every vault operation resolves the path and checks it stays under the vault root.
  6. Semantic search — a 15-minute cron diffs changed files by mtime, chunks by heading, batch-embeds, stores float-32 blobs; search unpacks and does pure-Python cosine, top-k. No numpy, no vector DB.
  7. Task round-trip — SQLite is truth; tasks render to markdown with ^tID block refs; the 10-minute sweep parses ticked boxes back to the DB and re-renders.
  8. Cron plane bypasses MCP — independent idempotent scripts (index, sweeps, 21:30 digest) hit SQLite and the filesystem directly.
  9. Dashboard — a read-only FastAPI plane over logs, cron config, and system metrics. It observes; it never mutates.
  10. Model registry — one file maps each job to model, endpoint and key; swapping OpenAI for a local model is a one-line change.
0:00

Drill cards

Why MCP instead of a REST API or function calling?

Decoupling. MCP makes the tool layer a standard protocol surface, so any capable model — Claude, GPT, a local model — can drive the same tools without me touching tool code. The runtime decides; the tools do. Schemas come free from typed signatures, and swapping the brain is configuration, not a rewrite.

tap to reveal

Why SQLite plus markdown instead of one database?

Different memory types. Tasks, moods, activity are structured and queryable — SQL. Reflections and long-form notes are narrative — human-editable markdown that opens in Obsidian. A single-file database gives zero-ops portability and backup by copying. The seam between them is the interesting part: the task mirror keeps both worlds in sync.

tap to reveal

How does semantic search work, exactly?

Indexing: a cron diffs the vault by mtime, chunks changed markdown by heading, batch-embeds with text-embedding-3-small, and stores each vector as a packed float-32 blob in a SQLite table. Query: embed the question, full-scan the chunks, unpack each blob, cosine in pure Python, return top-k with an optional path filter. Brute force is the right call for a single-user vault — thousands of chunks, milliseconds. At millions I’d reach for a real index. No numpy, no vector database, nothing to operate.

tap to reveal

Walk me through the task sync.

SQLite is the source of truth. Every task write re-renders an Obsidian-compatible tasks.md where each line ends in a caret-tID block reference — that ID is the join key between the file and the database. A ten-minute cron parses the file, finds ticked checkboxes, closes those task IDs in SQLite, and re-renders. So the same task can be closed by the agent, by SQL, or by ticking a box on your phone’s notes app — and they converge.

tap to reveal

The model chooses file paths — what stops it escaping the vault?

Every vault operation resolves the path and requires it to stay under the vault root — and there’s a test for traversal and absolute paths. Then I’ll volunteer the flaw before they find it: the check is a string-prefix comparison, so a sibling directory sharing the prefix — vault versus vault-secret — would pass. The fix is Path.is_relative_to, one line. Knowing exactly where my own guard is weak is the point of owning the code.

tap to reveal

What breaks on a fresh clone? (Know this cold.)

Three tools shell out to private helper scripts I didn’t open-source — calendar, voice transcription, and the database CLI — so on a bare clone those return structured errors. That’s deployment glue to my personal setup, and the README currently presents them as working capabilities, which I’d correct. What is fully real and tested in the repo: the tool layer, the database layer, the semantic indexer, the digest builder and the dashboard.

tap to reveal

Is the free-form SQL tool actually read-only?

Honestly: it’s a startswith-select check in the MCP wrapper, not database-level enforcement — no read-only pragma on the connection, and it wrongly rejects legitimate WITH queries. It stops accidents, not adversaries. Production fix: open the connection in read-only mode and enforce at the database. I flag this myself because “safety in the wrapper, not the store” is exactly the kind of gap I’d hunt for in review.

tap to reveal

What happens when a scheduled job fails?

Each cron script is independent and idempotent — the indexer diffs by mtime, the sweeps use flags and duplicate checks, the voicenote sweep even skips files under five minutes old to avoid racing a live recording. A failure logs to stderr and exits non-zero without corrupting state; the embedding indexer retries batches three times with backoff. What’s missing is central alerting — right now a silently failing cron is only visible on the dashboard, and I’d add a heartbeat check first.

tap to reveal

Why errors-returned instead of exceptions?

Because the caller is a model mid-loop. A raised exception kills the tool call and can derail the whole agent turn; a structured ok-false with an error message is something the model can read, reason about, and route around — retry, ask the user, pick another tool. The failure mode becomes data instead of a crash.

tap to reveal

The dashboard binds 0.0.0.0 with no auth — defend that.

I won’t fully defend it — it’s a home-lab assumption: trusted LAN, single user. It exposes metrics and logs read-only, but that includes config and cost data, so shipped anywhere real it needs to bind localhost behind a reverse proxy with auth. It’s also Linux-specific — systemd and journalctl — and degrades to blanks elsewhere. Right scope for a personal agent, wrong scope for production, and I know which changes cross that line.

tap to reveal

Own it before they find it

  • Unshipped helper scripts. Calendar, transcription and the DB CLI live in my private setup; three tools error on a fresh clone. README needs correcting — say it before they clone it.
  • Path guard is prefix-string. Sibling-directory bypass exists; fix is is_relative_to.
  • Dashboard unauthenticated on 0.0.0.0. LAN assumption; not production posture.
  • Duplicated logic. The tasks.md renderer exists twice; one table’s schema is created in three places. Maintenance debt I’d consolidate.
  • “Read-only SQL” is a wrapper check. Not enforced at the database.
Never claim:
  • Working calendar or voice transcription from the public repo alone.
  • “Integration tests” — the 22 tests are unit + CLI round-trips; nothing hits live providers.
  • Hardened SQL sandboxing.

Numbers to know cold

24MCP tools (exact)
22pytest tests
~2,830lines of Python
6tool groups
4cron jobs
0numpy / SDKs — hand-rolled urllib

Stack: Python 3.11 · FastMCP (mcp≥1.2) · FastAPI · SQLite · OpenAI · Exa · Firecrawl · cron/systemd

Social Content Agent (mandem)

Your production story — it runs a real Instagram fan page, live on a VPS. Public repo: ai-social-content-agent (a curated slice — say so upfront, always).

60-second pitch

This is a human-in-the-loop content engine that runs a real football fan page on Instagram — live in production on a VPS under systemd, with an operations runbook and real incidents behind it.

A plain-Python conductor — deliberately no agent framework — polls about thirteen RSS feeds and Reddit every two hours, ranks stories, and pitches me a numbered menu over Telegram. Nothing drafts until I pick; nothing publishes until I tap Post. Six narrow LLM calls do all the language — a premium model owns the brand voice, a cheap one does utility — and everything else is deterministic: ranking, image gates, Pillow compositing, publishing.

Images are never AI-generated — real photos only, verified by a vision model that fails closed, segmented for compositing. And my favourite piece is the publish path: an atomic SQL claim plus a three-way outcome model, so a double-tapped Post or a lost API response can physically never double-post. Two hundred and forty-four offline tests in the production system.

0:00

The whiteboard

Blind mode. Draw the two-gate pipeline from scheduler to LIVE permalink, out loud, 5:00. Then reveal and compare.
flowchart LR
  A["Scheduler loop
every 2h, 08:00-23:00"] --> B["~13 RSS + r/soccer
stdlib parse, sha256 dedupe"] B --> C[("SQLite WAL
news + draft state")] C --> D["Rank + diversify
menu of 10 to Telegram"] D --> E{"Human picks
or /post topic"} E --> F["Caption: GLM-5.2 voice
fallback Gemini"] F --> G["Image ladder: real photos
vision gates fail closed"] G --> H{"Gate 1: approve /
edit / skip"} H --> I["Deterministic Pillow
hero compose"] I --> J{"Gate 2:
Post / Cancel"} J --> K["Atomic claim ->
R2 -> Instagram"] K --> L["LIVE permalink
status: posted"]
  1. Scheduler — an in-process loop, every 2 hours, gated to 08:00–23:00 London, and it skips entirely while 2+ drafts are open. Not cron — one process, one flock lock.
  2. Sources — ~13 RSS feeds plus Reddit r/soccer, parsed with the standard library (no feedparser), deduped by hashing the URL into SQLite.
  3. Ranking — deterministic: heat score + keyword boosts and penalties + recency, capped at 3 per source for diversity. A numbered menu of 10 goes to Telegram. It never drafts on its own.
  4. Human pick — I tap a number, or type /post with a topic, which grounds itself in matching real news items first so the model reacts to reality, not invention.
  5. Caption — the voice slot is a premium model (GLM-5.2) with a shared persona and banned-word list; it falls back to Gemini on failure. Six narrow LLM calls exist in total — language only, never control flow.
  6. Images — real photos only, never generated. A gate stack rejects low resolution, wrong identity, stock watermarks, hidden faces and small subjects — and the vision check fails closed: only a confident yes passes.
  7. Gate 1 — approve, edit, or skip in Telegram. Edits rewrite the caption; the image is locked.
  8. Compose — deterministic Pillow: graded black-and-white background, auto-fit headline, segmented cutout with rim light and shadow, film grain, wordmark. No AI in layout.
  9. Gate 2 → publish — Post or Cancel. Publish claims the row with an atomic UPDATE (proceed only if exactly one row changed), uploads to Cloudflare R2, calls the Instagram API, replies with the live permalink.
  10. Outcome model — three ways: clean failure is retryable; a timeout after submission becomes post_unknown and is never auto-retried — a human reconciles. That's why it can't double-post.
0:00

Drill cards

Why no agent framework?

Deliberate. An earlier version was agent-driven and I replaced it: plain Python owns the workflow, and the LLM is confined to six narrow calls — captions, editorial metadata, edits, banter, reel lines. Language is AI; control flow, ranking, images, layout and publishing are deterministic code. That's what makes it testable offline, cheap to run, and predictable at 2am when it's posting to a real audience.

tap to reveal

How exactly can it never double-post?

Two mechanisms. First, an atomic claim: publish runs UPDATE … SET status='posting' WHERE the row is claimable AND has no Instagram media ID — and only proceeds if exactly one row changed, so concurrent taps lose cleanly. Second, a three-way outcome model: a clean API failure is retryable; but a timeout after submission — where the post might be live — becomes post_unknown and is never auto-retried; a human checks the page and reconciles. Lost responses are the classic double-post cause, and this treats them as their own state. It's all pinned by tests.

tap to reveal

Why real photos only, and how do the image gates work?

Identity safety: this posts about real footballers, and I decided the system must never misrepresent a real person — so generation is off by default and photos are sourced, then verified. The gate stack rejects low resolution, wrong player, stock watermarks, hidden faces, and too-small subjects — and the identity check fails closed: anything but a confident yes is a rejection. Passing photos get segmented for compositing, and Creative-Commons images can't publish without their attribution re-resolved from source.

tap to reveal

What does production actually look like?

A Lightsail VPS, a systemd user unit with Restart=always, secrets in an environment file, and an operations runbook with real traced incidents. The scheduler is in-process — one flock-locked process, no cron daemon — with one honest exception: the Instagram token refresh is an external cron on roughly a 50-day cycle, and it lives outside the repo, which I'd name as my biggest operational single point of failure. Deployment is manual scp plus restart — no containers, no CI/CD delivery. It works, and I know exactly what I'd harden first.

tap to reveal

How do you keep the brand voice consistent?

Model slots and shared constraints. The premium model owns the voice — captions and reel lines — because the voice is the product; a cheap model does utility work like editorial metadata and edits. Both run through one shared persona prompt and one shared banned-word list, so captions and reels can't drift apart — the list bans character-assassination words because the editorial rule is “banter the moment, not the man.”

tap to reveal

Tell me about a real production bug.

Silent caption truncation. The voice model would intermittently return empty or cut-off output, and it traced to a provider quirk: unless you explicitly disable the model's thinking mode, it burns the budget reasoning and returns nothing. Same class of issue on the Gemini side with reasoning effort. The fix is pinned in code and — the important part — pinned by tests, so a provider default change can't silently reintroduce it. Three separate production truncation bugs came down to this.

tap to reveal

What's the weakest part of the system?

Three things I'd name unprompted. The ranking is hand-tuned keyword heuristics — it works but it will age, and it's my thinnest-tested area: four tests, versus twenty-eight on video fetching. A row can strand in 'posting' if the process dies mid-publish after the claim — the design prevents double-posting but there's no automatic reaper, a human resolves it. And production runs on the OS Python while CI tests newer versions — an interpreter mismatch I'd fix with a container. None are mysteries; all are on the list.

tap to reveal

What's the test philosophy?

Offline by construction: 244 pytest tests where every network layer is monkeypatched — the suite needs zero API keys and runs in seconds, so it actually gets run. Live-world checks live separately in a smoke script that validates real keys, live feeds, and the media toolchain before a deploy. Unit determinism and runtime verification are different jobs, so they're different tools.

tap to reveal

Is the public repo the whole system?

No, and I say that first: the public repo is a curated slice showing the architecture — the production system is larger: about twelve thousand lines, 244 tests, plus a Reels subsystem that fetches video, rewrites lines in the brand voice, and composes 9:16 output with ffmpeg. I keep the full system private because it runs a live page with real credentials and a real audience. Happy to walk through any part of it on a call.

tap to reveal

Own it before they find it

  • Public repo is a curated slice. 46 files / 8 test files public vs 64 files / 26 test files / 244 tests in production. Say "curated slice" before anyone clones it.
  • Stranded 'posting' rows need a human. No reaper for a mid-publish crash; post_unknown resolves manually by design.
  • Ranking is heuristic and thinnest-tested. Hand-tuned keyword lists; 4 tests on the most subjective logic.
  • Prod interpreter ≠ CI interpreter. VPS runs system Python; CI tests newer. Containerizing is the fix.
  • Token refresh is an off-repo cron. A silent ~50-day single point of failure; I'd bring it in-repo with alerting.
Never claim:
  • Multi-platform posting — it publishes to Instagram only; TikTok/YouTube/Reddit are reel sources.
  • The production numbers as the public repo's numbers — cite 244 tests as "the production system", and the public repo as the slice.
  • A product/multi-tenant story — it's single-operator by construction, and that's the right scope.

Numbers to know cold

244pytest tests (production) · 26 files
~12,300LOC Python (production)
6LLM calls total — language only
2human gates before anything posts
~13RSS feeds + Reddit
3publish outcomes (ok / retry / unknown)

Stack: Python · SQLite WAL · Telegram Bot API · Instagram API · GLM-5.2 + Gemini · BiRefNet (fal.ai) · Pillow · yt-dlp + ffmpeg · Cloudflare R2 · systemd on Lightsail

Mira Studio Engine

Your cost-engineering story, with a live product site (miracontent.studio). Repo: mira-studio-engine-public — an explicit public slice.

60-second pitch

This is a product-photography engine I built and used for my own store's shots — local-first TypeScript, with a CLI and a React studio driving the exact same functions.

A brief becomes scene plans, and then the part I'm proudest of: a deterministic prompt-lint gate that refuses to spend a cent unless the prompt has a focal length, an f-stop, a light direction and quality, and a photographer citation — and it bans generic AI filler like “cinematic lighting.” Craft the operator might forget, turned into code that always runs, before any money leaves.

Generation is tiered — cheap drafts, premium finals — with scene one anchoring the campaign and fed as a reference into every other scene for visual consistency. Every take is cached by a content hash over model, prompt, size, references and take number — so re-running an unchanged scene is provably zero dollars. A human picks winners; picks append to a taste file, and tagged winners become brand-library references that bias future runs. One hundred and thirty-three tests, with honest per-step cost tracking.

0:00

The whiteboard

Blind mode. Draw brief → lint gate → anchored generation → cache → selection → brand memory, with the feedback loop. 5:00, out loud.
flowchart LR
  A["Brief / config
validated"] --> B["Look + refs +
library winners"] B --> C["Scene plans
model, size, takes, cost"] C --> D{"LINT GATE
refuses to spend"} D -->|pass| E["Anchor scene 1
fresh / winner / pinned"] E --> F["Other scenes, conc. 3
anchor fed as ref"] F --> G{"Content-hash
cache?"} G -->|hit| H["$0 copy"] G -->|miss| I["Gemini generate
retry, backoff"] I --> J["Advisory critic
never rejects"] H --> K["Atomic run.json vN
gallery + memory"] J --> K K --> L["Human picks winners
taste.md + library"] L -.->|feeds next run| B
  1. Brief in, validated — config type-guarded field by field; the studio UI and CLI call the same functions.
  2. Look + memory resolution — a saved brand “look” loads as defaults; up to three past tagged winners join as style references; scene-type bundles append to prompts.
  3. Scene plans — per scene: references, aspect, model tier, size, take count, and a unit cost — so the dry run can price the whole campaign with zero API calls.
  4. The lint gate — deterministic rules: 200–600 chars, a focal length, an f-stop, light direction and quality, a photographer citation from the catalog; bans generic filler. Errors throw before any spend.
  5. Anchor phase — scene one generates first (or resolves from the parent version's human-picked winner, or is pinned), then is fed as a reference image into every other scene. Campaign-wide consistency by construction.
  6. Generation — remaining scenes at concurrency three. Per take: content-hash lookup first — a hit is a zero-dollar file copy; a miss calls Gemini with layered retries and immediate abort on content-policy blocks.
  7. Advisory critic — optional vision pass flagging technical defects only — garbled text, melted edges. It advises; it never overrides the human.
  8. Persist — an immutable version directory: atomic run.json snapshot (config, prompts, refs, cost steps, errors), latest symlink, contact-sheet gallery, brand-memory record. Fail-loud unless partial runs are allowed.
  9. Selection — the human picks winners; that writes winners.json and appends the reasoning to a taste file. Tagging a winner copies it into the brand library.
  10. The loop closes — looks, taste and library feed the next run's defaults. The brand becomes a reusable asset, not re-derived prose.
0:00

Drill cards

Why lint prompts before generating?

Because image generation bills per attempt, and a structurally weak prompt is a guaranteed wasted spend. The linter encodes photography craft as rules — focal length, f-stop, light direction and quality, a named photographer's style — and bans the generic-AI vocabulary that produces generic-AI images. It runs before any API call and throws on errors. Same gate closes the AI-enrichment loop: draft, lint, feed errors back, retry up to three times. Money only moves when the prompt has passed.

tap to reveal

How is a re-run “provably zero dollars”?

The cache key is the content: a SHA-256 over model, image size, prompt, aspect ratio, the hashes of every reference image, the anchor hash, and the take number — and the filename is the key, so there's no index to corrupt. If nothing changed, the hash matches, the bytes copy from cache, and the cost tracker logs it at zero. Change one word of one scene's prompt and only that scene re-bills. It turns iteration — the actual workflow of creative work — into the cheap path.

tap to reveal

How do you keep a whole campaign visually consistent?

Anchoring. Scene one is the campaign anchor — generated fresh, inherited from the parent version's human-picked winner, or pinned explicitly — and its output image is fed as a reference into every other scene's generation. Versions form an inheritance chain, so the working rule is “change one variable, not the campaign.” Plus up to three past tagged winners from the brand library ride along as style references.

tap to reveal

What is the “director”, exactly?

Not code — a two-thousand-line structured knowledge base: photography craft, scene grammars, failure modes, catalogs of photographers and grades. A human or an AI assistant reads it to write briefs; the only automated path is enrichment, where the skill is distilled into a system prompt for a chat model whose draft then has to survive the same lint gate. And I'm straight about the boundary: the public repo ships the skill's structure, while the full research teardowns and fix recipes are proprietary — that's the IP layer.

tap to reveal

Where does it fail badly? (Own this.)

Crash-mid-run. The run manifest is written once at the end, so a process death mid-run leaves a version directory of images with no manifest — invisible to the version resolver, no resume, only the cache softening the re-run. The studio's run registry is in-memory, so a server restart orphans in-flight status. And the filesystem store has no locking — two concurrent runs could compute the same version number. Single-user tool, so these are accepted risks — but I know each fix: write the manifest incrementally, persist the registry, lock the version counter.

tap to reveal

How honest is the cost tracking, really?

Honest mechanics, honest caveat. Every step logs against a per-model, per-size price table, dry runs price a campaign before spending, and cached scenes log zero. The caveat I volunteer: the table is hardcoded — verified against the provider's published pricing on a specific date — and it counts image output only, not input tokens. So it's a faithful tracker of a table that can drift, not a billing API. For a personal tool that's the right trade; for production I'd reconcile against actual invoices.

tap to reveal

Did clients use this? (The truth line.)

Exact framing, never crossed: “I built and used this for my own store's product shots. The demo brands are concept work, not client engagements — the README says so explicitly. I have not run measured client campaigns.” Then pivot to what's real: the engine, the gate, the cache math, the live product site. One overclaim here would poison every verified number in the portfolio — this is the discipline question wearing a sales costume.

tap to reveal

What do the 133 tests actually prove?

Pipeline wiring, not live-API success — and I make that distinction myself. The Gemini SDK is mocked, so the suite proves planning, linting, caching, versioning, selection and memory behave correctly, offline, on Node 20 and 22 in CI. Live generation is verified by use — I bring real output samples. Knowing exactly what your test suite does and doesn't prove is the difference between a number and an argument.

tap to reveal

Own it before they find it

  • Mid-run crash leaves orphans. Manifest written at the end; in-memory run registry; no FS locking (version-number race).
  • Cache never evicts. Grows forever, one full-res image per unique take. Needs a size cap or GC.
  • Lint gate is gameable. Regex presence-checks, not correctness — and it silently degrades if catalog files are missing.
  • Single vendor. “Model routing” is tiers of Gemini; a second provider is roadmap, not reality.
  • One squashed commit. Curated public release; real iteration happened privately. Point to the booking center for live history.
Never claim:
  • Client campaigns, engagement metrics, or “billboard-grade” as an achieved result — design intent only. The truth line: own store's shots; demo brands are concept work.
  • The Midjourney/Higgsfield teardown as a shipped artifact — it's private research; frame it as process.
  • Live-API verification as something the repo demonstrates — committed tests are mocked; bring real samples instead.

Numbers to know cold

133Vitest tests · 22 files (badge accurate)
~8,600tracked LOC (5.3k src + 2.1k skill)
$0cost of an unchanged-scene re-run
3×3worst-case retries per take
≤3library refs auto-injected per scene
2surfaces (CLI + React studio), same functions

Stack: TypeScript (strict) · Node · Gemini image models (tiered) · GLM/DeepSeek/OpenAI-compat enrichment · Express + SSE · Vite/React 19 · filesystem store, no DB · CI on Node 20/22

Doc-to-Action Pipeline

Your evidence-and-governance story — typed contracts, RBAC, evals. Repo: doc-to-action-pipeline. Best for document-heavy and enterprise-flavoured interviews.

60-second pitch

This is a document-to-action workbench: paste text or upload a PDF, DOCX or transcript, and it extracts decisions, risks, questions and action items — with every action item carrying a required supporting quote from the source document.

The contract is typed end to end: the model is pinned to JSON output at temperature 0.1, revalidated with Zod at every boundary, and anything missing an owner or a deadline — or that looks like a duplicate — is flagged needs-review and cannot be approved until a human clears the reasons. Approved tasks export to Notion or Slack with encrypted credentials, a simulator mode when credentials are absent, and an append-only audit trail plus per-run cost and ROI metering.

A nine-case eval harness asserts every quote is an exact substring of the source and that no owners or deadlines are invented — nine of nine passing against DeepSeek. And I'll volunteer the top production to-do myself: that grounding check and the approved-only filter currently live in the eval suite and the client — hardening both into the server path is the first thing I'd ship.

0:00

The whiteboard

Blind mode. Draw intake → extraction → review → export with the eval harness on the side, out loud, 5:00. Then reveal and compare.
flowchart LR
  A["Paste / upload
PDF, DOCX, transcript"] --> B["Parse route
12MB cap, per-type parsers"] B --> C["Clean text
one shot, no chunking"] C --> D{"Provider adapter
mock regex / DeepSeek"} D --> E["Zod validation
quote required per item"] E --> F["Tasks + review flags
owner, deadline, duplicate"] F --> G["Evidence panel
quote beside task"] G --> H{"Approval queue
reasons must clear"} H --> I["Export approved
Notion / Slack / simulator"] I --> K[("Audit + ROI
events, cost, minutes saved")] E -.-> L["9-case eval harness
substring grounding"]
  1. Intake — paste, or upload to the parse route: 12 MB cap, dispatch by type — PDF via pdf-parse, DOCX via mammoth, plus text, markdown, CSV and subtitle formats.
  2. Clean, no chunking — the whole cleaned document goes to the model in one shot. Say it honestly: long documents risk truncation; chunk-and-merge is the known fix.
  3. Provider adapter — deterministic regex mock by default (offline, zero-cost demos and reproducible evals); DeepSeek in JSON mode at temperature 0.1 behind an env flag, prompt from a versioned registry.
  4. Validation — model JSON must pass the Zod extraction schema; every action item requires a non-empty supporting quote. Bad shape is a 422, missing key a 503, provider failure a 502.
  5. Review flags — a task needs review if the owner or deadline is missing, or it's a probable duplicate (token-set Jaccard ≥ 0.42 against existing tasks).
  6. Evidence panel — the quote renders beside the task on click. The honest boundary: it displays the model's quote; it doesn't verify it against the source at runtime — the eval suite does that.
  7. Approval queue — reasons must clear before approve; status machine needs_review → approved → sent, with blocked and rejected; transitions diffed into append-only ApprovalEvent audit rows.
  8. Export — the client builds an approved-only payload for Notion (introspects the database, maps properties, adds a quote block) or a Slack webhook; missing credentials degrade to a simulator mode; stored tokens are AES-256-GCM encrypted. Own it: the approved-only filter is client-side — the server routes don't re-check status yet.
  9. Audit + ROI — every export attempt, automation event, token count and cost estimate is persisted; the app can answer "what did this run cost and how many minutes did it save."
  10. Eval harness on the side — nine cases across seven categories, asserting exact-substring evidence and no invented owners or deadlines; the committed run is nine of nine against DeepSeek.
0:00

Drill cards

How does the evidence system work — really?

Layered, and I present it precisely. The schema makes a supporting quote structurally required on every action item — Zod rejects extractions without one. The prompt demands an exact contiguous substring and forbids invented owners and deadlines. The eval harness asserts the substring property against real sources. What runtime does not do yet is re-verify the quote against the document — a fabricated quote from the model would flow through. That check exists in the evals; promoting it into the live path is my stated top production to-do.

tap to reveal

Where does the approval gate actually live?

Honest answer before anyone finds it: in the client. The UI computes review reasons — missing owner, missing deadline, probable duplicate — and only clears approval when they're resolved, and the export payload filters to approved tasks. But the server export routes accept any schema-valid payload; they don't re-check task status. RBAC still controls who can export, but the approved-only guarantee needs a server-side status check — a small change, and the first one I'd ship.

tap to reveal

Why is the default provider a regex mock?

Deliberate: the whole app runs offline at zero cost — demos need no keys, evals are reproducible, and the provider interface keeps API keys server-only. The real path is DeepSeek in JSON mode at temperature 0.1 with a versioned prompt. And I'm upfront that three other providers named in the database enum aren't implemented — the enum is forward-looking, the adapter supports two.

tap to reveal

Describe the RBAC design.

A permission matrix: five roles — owner, admin, approver, member, viewer — against six permissions like process documents, export tasks, manage integrations, manage members. Every API route calls requirePermission. The caveat I volunteer: in demo mode auth is disabled and everyone is effectively owner — RBAC is only real under Clerk. That's the standard demo-versus-production posture, and I say which mode is which.

tap to reveal

What do the evals cover, and what don't they?

Nine cases across seven categories — core extraction, classification, duplicate detection, long context, messy input, missing context, and safety — run by a custom Node harness against the live server with a real model key. The committed run is nine of nine against DeepSeek, with token counts recorded. What they're not: unit tests. There's one Playwright smoke script and zero unit tests — my rigour went into behavioural evals of the AI layer, and I'd add route-level unit tests first.

tap to reveal

Walk me through the data model.

Twenty-two models, nine enums. The spine: Document with its SourceFile, an ExtractionRun recording provider, tokens, pricing and the raw JSON, then Tasks with confidence scores and a self-relation for duplicates, TaskEvidence holding the quote, ApprovalEvent as append-only audit with before-and-after state, ExportDestination with encrypted config, ExportAttempt with full request-and-response payloads, and UsageMeter plus CostEstimate for ROI. Even the eval cases and results have persisted schema. It's designed as an auditable system of record, not a demo table.

tap to reveal

What happens with a 100-page document?

Today: risk. There's no chunking — the cleaned document goes up in one call capped at four thousand output tokens, so a very long input can truncate or lose items. There's a long-context eval case, but the architecture is single-shot. The fix is chunk-extract-merge with the existing Jaccard dedupe doing the merge — the dedupe machinery is already built, which is why I scoped chunking as a next step rather than a rewrite.

tap to reveal

What would a senior engineer criticize first?

Four things, and I'd name them before they do. The approval gate isn't enforced server-side. Persistence is browser-authoritative — localStorage owns state and syncs wholesale, so the server trusts client timestamps and multi-device use would clobber. The main workbench component is three thousand lines — it needs splitting into a state machine and API hooks. And testing is evals-plus-smoke, no unit layer. All four are scoped fixes, not mysteries — and honestly, knowing the refactor plan for your own monolith is a better interview answer than pretending you don't have one.

tap to reveal

Own it before they find it

  • Approved-only export is client-side. Server routes accept any valid payload; status re-check is the first production fix.
  • Evidence asserted, not runtime-verified. Substring grounding lives in the eval suite; promote it into the live path.
  • Default pipeline is a regex mock. Real LLM is DeepSeek behind an env flag; three enum providers unimplemented.
  • Browser-authoritative state. localStorage syncs wholesale; approval history reconstructed by snapshot diffing.
  • 3,265-line component, zero unit tests. ~43% of the source in one file; evals ≠ unit coverage; 6 commits in one push.
Never claim:
  • "The export routes only accept approved work" — that filter is client-side today.
  • Runtime evidence verification — it's eval-time only.
  • OpenAI/Anthropic/Gemini support — enum entries, not implementations. RBAC claims only under Clerk mode.

Numbers to know cold

9/9evals passing · 7 categories
22 + 9Prisma models + enums
~8,500LOC TypeScript
8API routes
5 × 6RBAC roles × permissions
0.42Jaccard duplicate threshold

Stack: Next.js 16 · React 19 · Prisma 7 · Postgres · Zod · Clerk · DeepSeek (JSON mode) · Notion API · Slack webhooks · AES-256-GCM secrets · Playwright

The Hard Questions

The career-change interrogation. Same rule: say your answer out loud before revealing.

Drill cards

“Tell me about yourself.” (60 seconds, know it cold)

Six years running business operations — a regulated appeals desk handling 250 cases a month, e-commerce ops, then IT support at Datto doing 200-plus tickets a month and a 500-user Microsoft 365 migration. In 2023 I founded a short-let property business in London and ran it for three years — 4.8 stars, 77 percent occupancy — and that’s where this started: I began automating my own operations with n8n and Claude, cut guest communication by five-plus hours a week, and realised the automation was the part I loved. So in 2026 I went full-time building production-grade AI systems — approval-gated support automation, lead-response and booking, an MCP agent platform. Now I want to do that inside a team, shipping to real users at real scale.

tap to reveal

“Who were your freelance clients?”

Straight answer, no flinch: “This year I chose to build a portfolio of production-grade systems full-time rather than take client work — depth over billable hours. Before that, my client was my own business for three years; I built and operated its automation with real money on the line. What I don’t have is experience in a team codebase — that’s precisely what I’m applying for.” Never imply clients that don’t exist. One probing follow-up would end the interview.

tap to reveal

“Did AI write this code?”

“Yes — I build with Claude Code, and I’m fluent in that workflow; in 2026 that’s how fast teams ship. What I own is the architecture and the judgment. Ask me why the safety check runs twice in the support desk, or why my LLM output shares a Zod schema with its deterministic fallback, or where the path guard in my agent is weak — I can tell you the flaws in my own systems unprompted, which is the real test of whether someone owns their code.” Then actually name a flaw. Confidence plus honesty beats defensiveness every time.

tap to reveal

“No CS degree, no employed engineering experience — why you?”

“Three reasons. I’ve been the user: six years in operations means I build for the person drowning in tickets, not for the demo. I ship: sixty-eight commits and forty-nine tests in three days is on GitHub. And I already think about what most junior engineers haven’t met yet — approval gates, audit trails, cost telemetry, failure modes — because I built systems where my own money was on the line. What I lack is team process — code review, production on-call — which is fast to learn inside a team and impossible to fake outside one. That trade is in your favour.”

tap to reveal

“Why is the support desk one squashed commit?”

“I iterated privately and squashed for the public release — the repo is the product, not the diary. If you want to see how I actually work, the booking center has the full history: sixty-eight commits over three days, each one a working increment. And I’ll walk through any file in the support desk right now — pick one.” Offering to open code live is the power move; use it.

tap to reveal

“What’s your biggest technical weakness?”

Pick the true one and pair it with evidence of self-awareness: “I’ve never operated under real production traffic — my systems have single-user scale, and my testing is uneven: forty-nine tests on one project, an eleven-case eval harness on another. I know what good looks like; I haven’t lived on-call with it. That’s a large part of why I want an employed role rather than another year solo.” Never answer “I work too hard.”

tap to reveal

“What would you do in your first 30 days?”

“Ship something small end-to-end in the first two weeks — the fastest way to learn a codebase is to change it. Read the system by fixing real tickets. Sit with the people who use what the team builds — that’s the operations instinct. By day thirty I want to own one workflow completely, with an observability or approval story attached, because that’s the shape of value I already know how to deliver.”

tap to reveal

“Salary expectations?”

Contract: “£250 to £350 a day inside IR35, depending on scope.” Perm: “Mid-forties to high-fifties depending on the role and learning curve — at this stage the team and the production experience matter more to me than the last five thousand pounds.” Then stop talking. Don’t negotiate against yourself, don’t apologise for the number, and never give a number lower than the range you stated.

tap to reveal

Standing rules

  • Never invent clients, users, or numbers. Every number you cite is in this trainer because it’s verified in code.
  • Own gaps before they’re found. A flaw you name is expertise; a flaw they find is a lie of omission.
  • The MIRA story is the spine. Operator → automated own business → full-time builder. Every answer can route back to it.
  • Offer to open code live. It’s the one move no bluffer makes.

Progress saves on this device — and syncs across devices once a sync key is set on the web app. Cards go green at box 4+; drill daily until every tab reads full. Sources: verified architecture briefings against the actual repos, 22 July 2026.