Peter Wyatt

Case study

The bug that was lying for me

In October 2026 I audited the form-filling code in a job-application pipeline I had built for myself, and found this:

} else if (/experience in|experience with|familiar with|proficient|knowledge of|skills in|background in/i.test(text)) {
  pickValue = "Yes";
}

Any yes-or-no question whose text matched one of those phrases got "Yes." Have you worked with this. Are you proficient in that. Do you have a background in the other. Yes, every time, with no reference to anything true about me. It is a reasonable-looking heuristic, and what it actually did was fabricate qualifications, and it had been live across a batch of 215 automated submissions before I read it closely.

I deleted it and left this where it had been:

// DELIBERATELY REMOVED: a blanket rule that answered "Yes" to any
// /experience in|experience with|familiar with|proficient|.../ question.
// That claims experience I may not have. It is a fabrication, and it ran
// on 215 auto-submitted applications. Skill and experience questions must
// be answered from the fact file or left for a human. DO NOT REINSTATE.

The replacement is not a cleverer pattern. Those questions are now answered from a single canonical fact file or handed to a person. The system got slower and less autonomous, and that was the right trade, because an autonomous system authorized to speak for you is only as good as its worst answer.

I lead with this because it is what I would want to know about someone building agent systems. Every demo works. The question is what happens when you discover your own system has been saying things that are not true, and whether you built it so that you could discover that at all.

What the system is

An end-to-end job-application pipeline, built for my own use. Eighteen ingest adapters pull from public job APIs, nine of them applicant tracking systems and the rest boards, aggregators and feeds, each normalizing a different payload shape into one record type with fingerprint-based deduplication. The store holds 73,431 postings across 1,217 companies ingested between April and October 2026, with tables for postings, applications, contacts, interviews, events and rate limits. On top of that sits a hard-requirement filter, a model-scored fit pass that reads the posting against the fact file, a tailoring step that renders a resume variant per posting, a factuality gate, a browser-automation form filler that extracts a form's fields and plans answers before touching them, and an approval loop that delivers each finished package for a one-tap approve or reject. Unattended batch submission is switched off.

The other two defects in the same audit

It skipped every question that already had an answer. That is exactly backwards: a pre-filled answer is the one nobody consciously chose. One large job board had stored, from some earlier application, that I could not provide proof of legal right to work and that I needed sponsorship. Both are false and both are automatic rejections. The filler saw the questions as answered and moved on. It now reads the current value and corrects it when it holds a known-true answer, logging every correction it makes.

And the work-authorization matcher was too narrow while sitting below the experience rule in the same if-chain, so common phrasings like "proof of your legal right to work" fell past it into the wrong branch. It now has a dedicated pattern for authorization language, plus explicit branches for age, driver's license and accuracy attestations.

All three are one failure wearing three coats: a chain of regexes that looked like it covered the space, applied at scale, with nothing reading the output. The fix was partly better patterns and mostly a different posture. Narrow the autonomy, and make the uncertain case stop and ask.

The guardrails that came out of it

A factuality layer now runs before any submission. It checks generated text against the canonical fact file for employers that are not in it, tools that are not in it, fabricated dates, and a block list of claims that are specifically false for me: a CRM I have never used, an inflated year count, a language fluency I do not have, a degree described as conferred when it is in progress. It is heuristic and its own docstring says so. It exists because the generator is a language model, and a language model will write a fluent sentence about a tool you have never opened.

The submission guards detect captchas, login walls, forced account creation and bot walls, and stop. The code says why: solving or bypassing them is against most applicant-tracking and employer terms of service and legally risky. The guard also distinguishes a visible challenge widget from ambient invisible anti-bot scripts, because conflating the two makes the system quit on sites that would have worked fine.

An 8 GB ceiling as a design constraint

Separately I run a 4,194-line agentic assistant as a chat bot over local models: nine tools, including web search, file operations and command execution, behind a structured tool-call protocol that supports multi-step reasoning chains and caps out at five iterations per turn. It switches between 3B, 7B and 14B parameter models based on available RAM.

That switch is not a flourish. All of this runs on an 8 GB laptop that is routinely several gigabytes into swap, and a ceiling that low stops being an annoyance and becomes architecture. The forecasting engine in my other project has zero runtime dependencies and is a pure function. The assistant picks a model that fits instead of the model it would prefer. Nothing runs parallel builds. Working inside a ceiling taught me more about system design than headroom would have.

LLM failure modes I have actually hit

Hallucination. Fabricated employers, tools and dates. Countered with a canonical fact file plus a gate that blocks, not with more emphatic prompting.

Instruction drift. A long session quietly stops honoring a constraint stated at the top. The durable fix is to move the constraint out of the prompt and into code that can fail, because a sentence asks and a gate refuses.

Tool-call errors. Malformed arguments, a plausible-but-wrong path, the same call repeated in a loop. Countered with a structured protocol, a hard iteration cap, and never letting a failed call be read as a successful one.

Context overflow. The interesting failure is not the error, it is the silent degradation before it, where the model still answers confidently from a truncated view. Countered by retrieving a small explicit slice instead of pouring in a repository.

Persona drift. A long-running assistant starts agreeing with everything and treating its own earlier output as established fact. Countered by re-grounding each turn in files rather than in the transcript.

And the one underneath all of them: a system with no step that can refuse will ship every bad answer it produces. The form filler had no such step. That is why it took a human audit to find a bug that was, functionally, lying.

What running the monitoring taught me

Thirty-seven scheduled jobs are loaded on that laptop handling ingest, watchdogs, monitoring and digests. Three lessons were paid for rather than read:

I also run Model Context Protocol servers so the assistant reaches local data sources through a typed schema instead of scraping around for them.

Applicant tracking systems, from the outside

Writing nine ATS integrations teaches you the category from the customer side. Every one carries a different idea of what a job, a posting, a candidate and a question are: different pagination, different field naming, different custom-question models, different guarantees about what stays stable between fetches. I have read a lot of ATS JSON and a lot of ATS DOM, and I have views on which data models made the application experience coherent and which made it hostile. That is domain knowledge about a product category, from the position of someone who had to integrate with it rather than sell it.

How this was built

AI-assisted development. I do not write this code from scratch in any language. I specify the system, direct language models to generate and debug it, read and evaluate what comes back, and own the outcome: the architecture, the trade-offs, the audits, and the decisions about what the system is allowed to do on my behalf.

The form-filler defect is the clearest statement of what that is worth. The model wrote the regex. The judgment that the regex was a lie was mine.