Inspiration
I was a Navy air traffic controller. The whole job is watching things that only go wrong when nobody is looking, and knowing which of the forty things on your scope is the one that matters right now.
I currently have fourteen builds and six deadlines running at once. Every one of them has a rulebook, and every rulebook has a clause that will disqualify you quietly. Not a hard clause. A boring one. A video set to unlisted instead of public. A repository nobody shared. A category never selected on a form.
Nobody loses these competitions because their idea was bad. They lose because at eleven at night on the last day, one box was unticked and there was no one on watch.
So I built the watchstander.
What it does
Tower reads a competition rulebook once and turns it into a set of machine-checkable requirements, each with a severity and a specific place to look for evidence. Then it runs every thirty minutes for weeks, unattended, checking your live repository, your public URLs, and your video against that set.
When something crosses the line, it does not send an email you will not read. It calls your phone and tells you, in a sentence, what is about to disqualify you and how long you have.
The part that matters more: when you fix it, Tower calls back and stands down its own alert. And when Tower was wrong to call in the first place, it says so. Most verification agents only ever add findings. Tower is built to take one back.
Three states carry that distinction, and they are not the same admission:
- RESOLVED — Tower called, the problem was real, you fixed it
- RETRACTED — Tower called, and the evidence was already in place. Tower was wrong.
- UNVERIFIED — Tower could not actually check. Not satisfied. Not unmet. Unknown.
That third state is the spine of the whole system. An agent that cannot distinguish "everything is fine" from "I could not see" is worse than no agent, because silence reads as an all clear.
How I built it
Four agents, deterministic control flow.
- Intake reads the rulebook in two passes. Pass one extracts obligation statements verbatim, no interpretation. Pass two structures only what pass one found, so an invented requirement has nowhere to come from. Every requirement carries the words it came from, which makes hallucination checkable in seconds.
- Watch observes and nothing else. It makes zero model calls. It maps requirements to real tool surfaces (files in the live GitHub repo, public URLs, YouTube oEmbed, the Devpost project page) and reports what it saw.
- Analyst decides what an observation means. The state machine is a pure function. A model never decides whether to escalate.
- Escalation is a policy table, not a judgment call. Kill requirements call immediately. Major requirements wait until seven days out. Minor ones never call. Every call is idempotent, because a thirty-minute schedule without that guard phones you ninety six times a day about the same missing file.
Gemini is used in exactly two places: extracting the rulebook, and writing the sentence a human hears on the phone. It never chooses to wake you up. Control flow a model can talk itself out of is not control flow.
Stack: Gemini 3.7 Flash and Gemini 3.5 Flash via the Google GenAI SDK on Vertex AI, Cloud Run, Cloud Scheduler, Firestore, Twilio for voice.
One architectural note worth stating plainly: Vertex runs in the global region while Firestore sits in nam5, because no Gemini 3.x model was available in us-central1 on this project. That split was forced by empirical probing, not chosen. I found it by writing a script that tries every candidate model in every region and prints what actually answers, rather than trusting a model list.
Challenges I ran into
Eight defects, and every single one of them is the same bug wearing a different hat: the system claiming knowledge it did not have.
Requirement IDs were not stable across extraction runs. The same obligation came back as
spinup-instructions-readmeone run andspin-up-instructionsthe next. My routing table keyed off those slugs, so nine of twelve requirements silently lost their tool surface and reported as unverifiable. Routes now match on content, because content survives re-extraction and model-generated slugs do not.Blank configuration read as failure. An unset URL reported UNMET rather than UNCHECKED, which meant Tower was one tick away from phoning me at three in the morning about its own empty config file.
Expired credentials retried silently for five minutes and then died. For an agent that runs unattended on a schedule, that failure is indistinguishable from a tick that found nothing wrong. Silence meant both "all clear" and "I am broken."
Conditional requirements were checked unconditionally. The rules say if your hosted site is private, then supply credentials. Tower dropped the "if," and the model helpfully invented the premise, narrating that I had a private website I do not have. Declared facts now default to false, so an unknown premise is never assumed true.
Narration was tied to state changes, so on the one tick where a call actually went out, the requirement was already open, nothing had changed, no sentence had been generated, and Twilio read a raw Python fallback string down the phone line, list brackets included.
Tower apologized for a call it never made. It reversed a finding that had never been escalated and said "please disregard our previous alert." There was no previous alert. Call history is now an input to narration.
A dry run mutated state. Merely looking at a tick would resolve a pending finding and silently consume it. Dry runs are read-only now.
The worst one. Tower reported a requirement SATISFIED on the strength of words appearing on Devpost's login page. An unsubmitted draft redirects anonymous visitors to sign-in, and that login page returns HTTP 200. My status check saw 200, believed it had fetched the project, and pattern-matched against a login form. A 200 is not the same as reaching the thing you asked for. There is now a login-wall detector and a regression test pinning it.
The eval harness was green the entire time defect 8 existed. Thirty passing assertions proved the logic was correct and proved nothing about whether the logic could run on real input.
Accomplishments that I'm proud of
I scored my own extraction and published the number that came back.
Intake was measured against a hand-written ground-truth key of 29 obligations from the official rules. The number moved three times, and the movement is the interesting part:
| Run | Recall | Precision | F1 | Why it changed |
|---|---|---|---|---|
| 1 | 62.1% | 90.0% | 73.5 | Baseline |
| 2 | 93.1% | 85.0% | 88.9 | Found a bug in my own scorer: it consumed each requirement once, so one requirement covering three bundled obligations scored two as missed |
| 3 | 79.3% | 90.0% | 84.3 | Audited the matched pairs by eye and caught four false credits from loose keywords. Revised the scorer against myself. |
79.3% recall, 90.0% precision, F1 84.3 is the honest figure. 96% severity agreement, 91% class agreement.
The misses have a shape. All three missed disqualifying requirements are standing conditions and legal clauses: the submission window, the no-edits-after-deadline rule, and original work ownership. Tower missed zero artifact requirements. It found the video, the diagram, the README, the repository, the description, the category, and the tech stack.
That is explainable rather than random, and the explanation is in my own design. The class taxonomy pushes toward "what must the entrant DO," and deduplication folds adjacent legal language together. A watchstander built to answer "is the work ready" is structurally weaker at "is the entrant eligible." I would fix that with a separate eligibility pass rather than by tuning the prompt.
The separate 32-assertion offline harness covers the state machine including both reversal paths, the escalation policy including idempotency, checker honesty, and login-wall detection. It runs in about a second with no credentials and no cost, so it can be re-run rather than believed.
What I learned
A monitoring agent's hardest problem is not detection. It is knowing what it does not know.
Every defect in this build was a variant of that. A 200 status code that was really a login wall. An unset variable that looked like a failure. An expired credential that looked like silence. A conditional whose premise got invented to fill the gap. In each case the system had an honest answer available (I cannot tell) and reached for a confident one instead.
That is not a Gemini problem. It is a design problem, and it is everywhere in agent work. The fix is boring and structural: make "unknown" a first-class value that propagates, refuse to let a checker return a boolean when it has not actually checked, and report coverage out loud so the gap is visible instead of implied.
Tower says it verified five of ten applicable requirements and names the five it could not. That is a worse-looking number than implying it checked everything, and it is the only number worth trusting.
What's next for Tower
- A separate eligibility pass. The measured weakness is standing conditions, so the fix belongs in architecture rather than prompt tuning.
- More rulebooks. Tower already handles multiple competitions; the code is not specific to this one. A watchstander tracking one deadline is a timer. Tracking six is a watch.
- Rule-change detection. When a rulebook is amended mid-competition, Tower should notice its own requirement set is stale and re-extract, rather than confidently watching yesterday's rules.
- A fail-fast heartbeat. If the scheduler stops firing, Tower should call to say it has stopped watching. An agent that notices its own absence is the version I actually want on the scope.

Log in or sign up for Devpost to join the conversation.