the assistant

How Ask Roger works.

status: live·members only

Ask Roger answers questions about Roger in his own voice, from his own records, and shows you where each answer came from. It runs at /ask and from the button in the corner of every page.

It is the same machinery as the JD reviewer below, pointed at a different job, and the two sections of this page share most of their answer. One model, open weights, on Roger’s own server. Nothing is sent to a hosted API, and the model has never been trained on his career: it is handed the relevant passages at the moment of the question.

What happens when you ask.

  1. A question Roger has answered. Your question is compared against a bank of questions Roger wrote the answers to himself. A close enough match, and far enough ahead of the next candidate, returns his words immediately. No model runs, and the answer is his rather than a reconstruction of his.
  2. Otherwise, retrieval. The question is turned into a vector and the nearest passages in the career corpus are pulled out, alongside a short sheet of career facts that every question gets: tenure, titles, dates, credentials. Those are the things retrieval is worst at and questions most often turn on.
  3. Then the answer. The model is given the facts, the passages and the conversation so far, and asked to answer briefly in the first person. It streams back a sentence at a time.
  4. Sources. Published passages that fed the answer are listed under it and link to the piece they came from, so you can read the thing itself rather than the summary of it.

It will tell you when it does not know. Asked something the records do not cover, it says so instead of producing something plausible. It will not discuss compensation, references, anything confidential to an employer, or Roger’s personal life, and points you at him directly for those.

It reads more than the reviewer does. A posting submitted to the reviewer cannot reach the private half of the corpus. A question here can: it is Roger’s own record answering about Roger. Private material may inform an answer and can never be quoted at length, named, or listed as a source, which is enforced where the sources are built rather than by instructing the model. Anything he does not want speaking for him at all is excluded from the assistant outright.

It is slow, and that is real. An answer takes a few tens of seconds. The whole thing runs on one small server with no graphics card, shared with the reviewer, and the honest reason it is not faster is that it is not worth renting a bigger machine to shave off twenty seconds. Questions from the bank return instantly; a question asked while a JD review is running will be slower still, because the review has the machine.

Conversations are kept so Roger can read what was asked and grade the answers, which is how the assistant is corrected. The mechanism is the one described under “How it gets better” below, and it works the same way here: when an answer is wrong, the usual fix is that no document says the thing plainly, and the document gets written.

the other half

The JD reviewer.

The review behind /jd-upload reads a posting rather than a question, and draws on the same career corpus. It works like this.

  1. Requirements. The posting is broken into checkable requirements, each marked must or preferred and weighted 1 to 3. Clauses like “or equivalent” are kept as written.
  2. Evidence. For each requirement the reviewer retrieves the most relevant passages from Roger’s career corpus and adds a short sheet of career facts, so the judge always sees the basics.
  3. Verdicts. A judge model reads that evidence and returns one verdict per requirement: met, partial or unmet, with a one-sentence reason. Each requirement gets its own call, so the model never has to hold the whole posting at once.
  4. Score. The score is computed in code from those verdicts, not guessed by the model. Met counts 1, partial 0.5, unmet 0, each weighted. Must-have items always count against the total; preferred items only count when they are evidenced.
  5. Fit category. The score lands in one of five categories: very strong (≥ 0.85), strong (≥ 0.70), possible (≥ 0.60), weak (≥ 0.40), and very weak below that. Roger can adjust the bands; the numbers here are the ones in force.
  6. Résumé. When the fit is strong or better, a second model writes a two-page résumé for that posting. Every claim in it carries a source id that is checked in code against the corpus before the résumé is rendered as a PDF. The PDF opens without a password but is locked against editing.

Reviews run one at a time on Roger’s own server and usually take 30 to 60 minutes, sometimes longer. You can close the page: your submissions list on /jd-upload reopens a review, and the outcome is emailed to you.

Which model, and what it is not.

One model does the reading: qwen3:4b-q8_0, four billion parameters at eight-bit precision, open weights, served through Ollama on Roger’s own server with an 8,192-token context. Retrieval uses a second, smaller model, nomic-embed-text, which turns text into 768-dimension vectors stored in Postgres with pgvector. Nothing is sent to a hosted API. No posting you submit leaves that machine.

The model is not trained on Roger’s career. It has never seen it in training and nothing about it has been fine-tuned. It is a general open-weight model that is handed the relevant passages at the moment of the question and asked to judge one requirement against them. That is a deliberate choice rather than a shortcut: a model trained on a career can only be corrected by training it again, while a model that reads a corpus is corrected by fixing the corpus, and you can see which passage produced which verdict.

It is also not allowed to decide the outcome. The model returns a verdict per requirement and a sentence of reasoning. Every number, the score, the weighting, the threshold, is computed in code from those verdicts. When the model has been asked to reason about something it is reliably bad at, that judgment has been moved out of the prompt and into code: how long a span of years is, whether a named company actually appears in the evidence, and whether a résumé line is carried by the source it cites.

How it gets better.

Four mechanisms, in the order they matter.

  1. The corpus gets fixed. Most wrong verdicts are not the model reasoning badly. They are the model failing to find something because no document says it plainly. When that happens the answer is to write the missing document, not to adjust the prompt.
  2. Roger grades the verdicts himself. Every verdict is logged with the exact evidence the model saw and the model and prompt version that produced it. He marks each one agree, disagree or not enough evidence to judge, in his own words. Disagreements are counted in two directions, because a reviewer that is too harsh and one that is too generous need opposite fixes.
  3. A fixed set of postings guards every change. A group of real job descriptions, some Roger applied for and some drawn at random from job boards, each carrying one claim: he can do this job, or he cannot. Any change to a prompt, a model or the corpus is scored against the whole set before and after. A change that improves one posting and quietly breaks another shows up as a number, not as a feeling.
  4. Rules move from prose into code. When instructing the model in words fails twice, the judgment is taken away from it and written as a rule that runs the same way every time.

A fine-tuned adapter, trained on the graded verdicts, is planned rather than built. It is listed here as a plan so that nothing above reads as a claim about something that exists.

where that stands today

83.9% agreement, over 31 verdicts Roger has graded by hand
4 outright disagreements, of which 4 were the reviewer refusing to credit work he can evidence and 0 were it crediting work he cannot. The second number is the one that would matter to you, and it is published whatever it says.
10 of 10 postings on the expected side of the gate, with no inversions
From a set of 10 job descriptions, 6 of them drawn at random from job boards rather than picked. The closest the two groups came was 0.179. Last run September 30, 2026 on ollama:qwen3:4b-q8_0.

These are read live from the same tables Roger reads. They are not a good look on purpose; they are the numbers, and a claim to be honest about evidence is worth less than the figures it is currently failing on.

What your posting is read against.

The corpus has a public part and a private part. The private part holds client work and material under agreement, and a posting submitted through this site cannot reach it: the restriction is enforced in the database query, not by asking the model nicely. Your review is drawn from the public documents, plus two files about Roger himself that every review uses, a sheet of career facts and his master résumé.

That is a real limit and worth stating plainly: a review here sees less than Roger does when he runs the same posting for himself. It is the reason a strong result comes with a résumé and a weak one comes with Roger reading the posting personally.

Your posting is stored so the review can be reopened and so Roger can see what was asked. It is never used to train anything and never shown to anyone else.

Ask it twice and the answer is the same.

Submit the same posting against the same corpus and you get the same verdicts and the same score. Not close to it. The same number. The table below is the check: the postings scored in the two most recent evaluations, before and after the reviewer’s prompt was rewritten, to three decimal places.

Four things make that true, and none of them is the model being reliable. The judge runs at temperature zero, so it is not sampling. Its answer is constrained to a fixed shape. That answer is one of three words, met, partial or unmet, not a number. And the score is arithmetic performed in code on those three words.

The model itself is not deterministic and this does not claim it is. Its intermediate reasoning does vary between runs on identical input. What the design does is put the decision somewhere that variation cannot reach: it would have to be large enough to move a requirement from met to partial before it could move a score at all.

Which matters for a practical reason. If you disagree with a result, it points at one requirement and the passage behind it, and you can run it again and get the same thing to argue with. And when a number does move, something really changed: the corpus, the prompt, or the model. All three are recorded with every run.

the same postings, scored before and after a prompt rewrite

postingbeforeafter
a role he applied for1.0001.000same
drawn at random: Process Control Engineernot in that run1.000new
a role he applied fornot in that run1.000new
drawn at random: General Manager, Commercial Manufacturing Facilitynot in that run0.929new
a role he applied fornot in that run0.929new
drawn at random: Automation Engineer (Manufacturing Support)not in that run0.929new
a role he applied fornot in that run0.821new
drawn at random: Structural Engineer, Data Centers0.6430.643same
drawn at random: Senior Power System Studies Engineer0.6430.643same
drawn at random: Machine Learning Scientist, Pretraining0.3570.357same

Read live from the evaluation records. The two postings Roger applied for are not named: telling those employers he applied, and what the reviewer scored him at, is his to disclose and is not something the evidence needs. The rest are public job advertisements drawn at random. A posting appears here only if both evaluations reached it.

the site itself

What this is made of.

Everything below is counted from the repository at build time, not typed in. The script that does it is scripts/build_stats.py, and the build fails if the committed figures differ from a fresh count. Clone the repo and run it; you should get these numbers.

82,219
lines of code

tests excluded

16,142
lines of tests

20% of the code

27
direct dependencies

Go, web and dev combined

27 direct dependencies covers a ConnectRPC API, vector retrieval, local model inference, PDF generation and a timed psychometric instrument. The documentation is 40% of the code volume, which is the number least likely to be matched elsewhere and the one this site would rather be judged on.

By category

CategoryFilesLines
CodeWritten by hand, tests excluded47082,219
TestsGo, TypeScript and Python9516,142
DocumentationSpecification, decision records, working notes12533,042
ContractProtobuf service definitions177,770
ConfigurationYAML and JSON305,003
GeneratedProtobuf stubs and lockfiles, excluded from the figures above7079,113

Languages, code only

  • Go 33,542
  • TSX 26,018
  • SQL 7,484
  • Python 6,189
  • TypeScript 5,569
  • Shell 2,497
  • Email templates 672
  • CSS 248

Who wrote it

935 commits in 39 days. That pace is not a person typing, and saying so here is better than leaving a reader to work it out and wonder what else the page is quiet about.

Of the 564 commits that changed something, 504 were written by an AI assistant working to a specification, 39 by Roger directly, and 21 by a dependency bot. He wrote the specification, reviewed every change, and merged 371 pull requests. None of that is a claim you have to take: clone the repo and run git log --no-merges --format=%an | sort | uniq -c. The --no-merges matters: a merge commit records who pressed the button, so without it Roger’s 371 merges count as authorship and the split looks close to even.

It prints one line per name, and there are more names than there are people. The assistant has committed as Claude, Claude Opus 4.7 and Claude Opus 5 as the model changed, Roger as Roger Edward Henley II and Roger Henley, and the bot as dependabot[bot]. Add those groups up and you get the three figures above.

The skill being demonstrated is not typing speed. It is knowing what to specify, what to check, and which failures matter: the 186 tracked requirements, the 15 checks that run on every push, and the decision records below are the part a model cannot supply.

Decisions, not volume

Line counts measure scope. These measure how it is held together.

186 requirements
tracked to a state: 136 shipped, 46 not built, and the gap published rather than tidied away
149 RPCs across 15 services
defined in protobuf, with lint, breaking-change checks against main, and a check that regenerating produces no diff
63 migrations
every one replayed on an empty database on every pull request
439 Go tests, 91 web, 30 Python
run on every push, alongside the type checks and the build
10 decision records
short, dated, and kept even when the decision was later reversed
6 checks written for this repo
content schema, design tokens, link resolution, no-JavaScript rendering, runtime version drift
9 containers
Caddy, the web app, the API, a Python sidecar, Postgres with pgvector, object storage, local model inference