Our compliance department is a git repository
We run AI assistants and websites for clients. Each one makes us a processor under GDPR, and since 2 August 2026 the transparency duties in Article 50 of the EU AI Act apply to the conversational ones. The rules keep moving and we have no compliance department. So we built one out of a git repository, a handful of scheduled workflows and one AI routine. This is how it works, including the month it went blind and said so.
What the watch has to do
Once a month it reads the law, works out which of our deployments a change touches, writes a report and delivers it to both founders. The scope is GDPR, the EU AI Act, ePrivacy and the European Accessibility Act. Nobody has to remember to start it.
It is an internal tool, and three files carry most of the weight:
PROFILE.mdis the compliance surface of every client deployment: what it runs, where data goes, and which kinds of legal change would affect it. New law is mapped against this file. The monthly routine reads it and is not allowed to modify it.SOURCES.mdis the tiered source list and the single home for URLs. The fetcher parses this file and nothing else.state.jsonholds seen items and open deadlines, so nothing is reported twice and nothing open is forgotten.
One report lands in reports/ per month. The output is agency work product, not legal advice, and the repository states that at the bottom of its own README.
Tier 1, Tier 2, and never Tier 3
Sources are ranked by authority, and the ranking is a rule rather than a preference.
| Tier | What it is | Examples |
|---|---|---|
| 1 | Authentic legal texts and rulings | EUR-Lex pages for the AI Act and the GDPR, the Official Journal, CJEU press releases |
| 2 | Official regulator guidance | EDPB news and guidelines, the Commission AI policy hub, national data protection authorities, the Accessibility Act page |
| 3 | Discovery only | Industry news, AI Act trackers, law-firm alerts |
Tier 1 and Tier 2 together are 13 sources. Tier 3 is never cited. A Tier 3 find is a lead: locate the primary source, verify it there, then report it. Every claim in a report carries a primary-source URL and one of three status labels: IN FORCE / APPLICABLE, ADOPTED NOT YET APPLICABLE, or PROPOSAL.
Each run also answers eight standing questions. Has a new act in the Official Journal amended or implemented the GDPR or the AI Act? Has the Article 50 timeline moved? Has the CJEU ruled on standard web tooling such as fonts, analytics, pixels or embeds?
Source lists rot quietly. When we finally read the snapshots instead of trusting the fetch, two “sources” turned out to be navigation menus: 97 lines, three of them mentioning a year. Worse, the AI Act and GDPR links used a consolidated CELEX form that does not exist for those acts, so the two texts the whole system rests on returned 404 on every run. The evidence check now flags any page that is short and carries almost no dated lines, so this does not depend on someone noticing again.
August 2026: every source blocked
The monthly routine is an AI model running in a cloud environment. On 1 August 2026 that environment refused every Tier 1 and Tier 2 host at the CONNECT stage: EUR-Lex, the CJEU, the EDPB, the Commission and the national regulators. Not one primary page could be read.
The routine did not pretend. The report opened with a section headed “Verification integrity notice (read first)”, listed each blocked host, and explained that every item below had come from web search alone. Each item was graded SEARCH-CONFIRMED or UNVERIFIED. The notice told the reader to re-run from an environment that could reach the sources before relying on the new PLAN and RADAR items, and pointed out that the first two ACT NOW items were deadlines already recorded from the previous run, so they did not depend on that month’s research.
It said so at the top, which is the only reason anyone noticed.
That line from the README is the design brief for everything that followed. A monitoring job that fails quietly produces the same email as one that worked. The only defence is a system that reports its own blind spots before it reports anything else.
One has a model, the other has internet
Once the failure was on the page, the cause was plain. The two halves of the job live in different places.
A GitHub Actions runner can reach EUR-Lex but has no model. The cloud routine has a model but cannot reach EUR-Lex. So the runner fetches and commits, and the routine reads the snapshots out of a git checkout. A checkout needs no network and cannot be blocked. That split is the architecture; the rest is plumbing.
The fetch job pulls all 13 sources, normalises each page to comparable text, diffs it against last month and commits. It is dependency-free and model-free on purpose, so it keeps working when an API key is missing or out of credit. It fails outright if any Tier 1 source is missing, because Tier 1 is the law itself and a month without it is not a month of monitoring.
The routine reads sources/last-fetch.json first and starts from what actually changed, rather than rereading everything and guessing.
The schedule, and the heartbeat that keeps it alive
When a report lands on main, a separate workflow tests the act-now counter, then runs the evidence check before anything is sent:
node scripts/check-evidence.mjs --max-age-days 40It fails if the newest snapshot is older than 40 days, if any Tier 1 source is unreadable, or if more than a third of all sources are unreadable. A failure stops the email. The comment in the workflow puts it simply: a reassuring email is worse than no email.
The heartbeat runs every Monday at 06:00 UTC and has two jobs. It detects a stopped watch by running the same 40-day freshness check. It also prevents one. GitHub disables scheduled workflows after 60 days of repository inactivity, and the fetch only commits when a source changed, so a quiet quarter could switch off every schedule, the heartbeat’s included. The heartbeat commits a small dated file each week to keep the repository active. It runs weekly rather than daily because the thing it watches moves monthly, and a job that runs every day is a job nobody reads the failures of.
The same thinking applies to the source-check email. It stays silent when nothing changed. A monthly mail that always says “no change” trains you to stop opening it, and then you miss the month that matters.
ACT NOW, PLAN, RADAR
The routine maps each finding against PROFILE.md: which client, which product, what must change. Then it sorts by severity into three buckets. Anything it could not read has to be named, not quietly dropped.
Deadlines within 60 days also become Todoist tasks. Todoist is the failure-proof channel; the email is the readable one. The report also proposes edits to our Data Processing Agreement template when a change affects it, and that template is only published after a guard script checks hard rules, such as no certification claims, because we hold none.
The September report included a small lesson worth keeping. One search result claimed that a 2026 implementing regulation amended the GDPR. It did not; it concerned a different regulation entirely. The report recorded it as a worked example of why search summaries are not authority.
What it still gets wrong
The split did not make September clean. The scheduled fetch did not fire on 1 September, so the routine found snapshots from 3 August, four weeks old, and still could not reach the sources itself. The report again opened with its integrity notice: it named the whole of August as a blind spot, graded every item by evidence (our own snapshot, search-confirmed, or unverified), and closed with the one thing a human had to fix. A fetch later that day brought back most sources, but all four EUR-Lex pages came back unreadable.
The README keeps a list of known limits, and they are worth repeating:
- Nothing verifies the report’s legal conclusions. The checks are mechanical. A confidently wrong report passes every gate.
- Thirteen sources is narrow. There is no CJEU full-text search and no national gazettes beyond the regulator pages.
The repository also keeps a defect log on purpose. One entry: the act-now counter reported zero above four items, one of them an Article 50 deadline due the next day, because its pattern matched 1. while reports write ### 1.. It now has tests. The log’s conclusion is the one we apply to every agent we build: the failure is always the system stating something reassuring that it had not verified. So that failure has to get past a check, not past a person.
If you are building something similar, the pattern transfers. Put the model where it does not need the network, put the network where there is no model, make git the boundary between them, and make every stage say what it did not see.