Inspiration
It's 11 pm and there's an injured kitten under a parked car. You search "cat rescue near me" and get ads, dead links, a Facebook page last updated years ago, and a phone number that rings forever. In that moment people give up, and the animal pays for it.
Whisker World turns that search into three seconds: find your city, click the cat, call a real rescue. But a directory like this rots the moment you stop maintaining it, and a wrong phone number is worse than none. So the real question became: can the whole lifecycle of this data, from planning a new city to proving every number and shipping it, run with no human in the middle?
What it does
Whisker World is a playful world map where every city has its own generated cat. Click it and a card shows real cat rescues with one-tap call links, sources, hours, and first-aid steps for strays.
Behind the map, a team of four GitLab Duo agents grows it by itself:
| Agent | What it does |
|---|---|
| Whisker Scout | Picks the next city from a curated backlog and opens an issue for it |
| Whisker Researcher | Chooses rescues only from evidence CI already checked, and posts a draft city file on the issue, with what it left out and why |
| Whisker Author | Turns the draft into a merge request that closes the issue |
| Whisker Fixer | Explains and closes anything stuck, so the loop never stalls |
CI then proves every phone number against its source page, runs SAST and Secret Detection, merges, deploys to Google Cloud Run, publishes a GitLab Release and a versioned dataset package, and finally posts "đž Live on the map" on the original issue, with a link straight to the new city. Nobody touched it.
Autonomy level: hands-off. In its first hours the autopilot added over ten cities, from Nairobi, Buenos Aires and Madrid to Lagos, Melbourne, Cairo and Vancouver, each with its own issue, merge request, green pipeline, Release and live summary.
How we built it
App: React 19, Vite, Tailwind, MapLibre GL with OpenFreeMap tiles, Framer Motion for the cats, and a small Express 5 API. A single Zod schema is the source of truth for the server, the frontend types, the validator and a published JSON Schema, so agents and humans write to a strict contract.
The loop, stage by stage:
| Stage | How it's automated |
|---|---|
| Plan | Whisker Scout opens a labelled issue per city, one city at a time |
| Create | Whisker Researcher drafts, Whisker Author opens the merge request |
| Verify | 300+ tests, schema validation, and verify-sources, a gate that re-fetches every sourceUrl and fails unless the phone number is on that page, "verified" entries are on the organization's own or an official site, vets show a real rescue program, and the coordinates match the city |
| Secure | Semgrep SAST and Secret Detection feed a security-gate job that blocks merge and deploy on any High or Critical finding; every fetch of an untrusted URL is guarded against SSRF |
| Release | Each map change becomes a tagged GitLab Release with notes, plus a summary posted on the original issue |
| Package | Each release publishes the dataset as a versioned package in the GitLab Package Registry |
| Govern | Merges are allowed only for the exact shape the Author produces, only after every gate job really ran and passed, with a protected token that never reaches an agent-writable branch |
| Monitor | A run log written by the agents, live smoke tests after every deploy, and the Fixer watching for stuck work |
| Configure | The entire loop is configuration as code: CI, flow definitions, the backlog, and on/off switches |
Research without hallucination: the flow sandbox has no web access, so a scheduled CI job asks Gemini on Vertex AI, grounded with Google Search, once per city. It then fetches every page Gemini cites and records whether the phone, name, address and cats really appear there. The agents only ever choose from that checked evidence. Cost: about one grounded call per city, inside the free monthly allowance.
Google Cloud: the app runs on Cloud Run, built by Cloud Build, deployed from GitLab CI with a least-privilege service account. A separate service account has only Vertex AI User.
Challenges we ran into
- Flows can only be started by a human. Duo triggers ignore bots, service accounts and other flows. The answer was a pipeline schedule owned by a person: its passing pipeline fires a "Pipeline events" trigger, which proved hands-off on the first unattended run. Because flows can't call each other, the agents hand off through comments on the city issue, which also makes every step readable.
- No web from the agents' sandbox. A probe agent showed
web_searchunavailable andcurlblocked by a proxy, so research moved into CI, and the agents work only from CI-checked evidence. - Our own security gate blocked our own code. Semgrep flagged every
fetch()with a variable URL as High-severity SSRF and stopped a production deploy, which is exactly its job. We reviewed each finding, documented the false positives, and fixed the real risk it didn't flag: untrusted URLs from Gemini and merge requests now can't reach private or cloud-metadata addresses, even through redirects. - The first "verified" data was too generous. Our seed cities predated the gate; running it over them found numbers missing from their cited pages. Then a Seoul shelter was rejected because our cat detection only knew Latin script (Korean pages say ęł ěě´), and two Buenos Aires vet clinics slipped in as "rescues". Each became a tested rule: 20+ languages, and vets only with evidence of rescue work.
- The map had never drawn its vector layers. Vite renamed MapLibre's module but never copied its worker, so seas, borders and labels silently failed in production. Only a faint relief image ever rendered. One
setWorkerUrl()call fixed the "washed-out map" a reviewer had flagged as a design problem. - Permissions in a shared group. We couldn't create branch rules or project access tokens, so the merge moved onto
main's own scheduled pipeline. That turned out safer: the merge token never reaches a branch an agent can push to.
What we learned
- Autonomy is a property of the gates, not the agents. The agents are useful because CI refuses anything it can't prove. Every failure we hit made the system stop, never ship bad data.
- Let agents decide, let CI do. Judgement (which rescue qualifies, how to explain a failure) went to Duo agents; anything mechanical or needing write power (fetching pages, retrying, merging) stayed in CI.
- Test the pipeline the way it runs. A throwaway trial branch for every CI change caught config errors and scanner findings before they could block production.
What's next
- A scheduled monitor that re-checks every city's sources and opens incident issues for the agents to fix
- A Reviewer agent on an @mention trigger, so a human can steer a fix in plain words
- An MCP server (
find_nearest_rescue), so any AI assistant can answer "who do I call?" from verified data instead of guessing
Built With
- cloud-build
- docker
- express.js
- framer-motion
- gemini
- gitlab
- gitlab-ci
- gitlab-duo-agent-platform
- google-cloud-run
- google-search-grounding
- maplibre-gl
- node.js
- openstreetmap
- react
- semgrep
- tailwindcss
- typescript
- vertex-ai
- vite
- vitest


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