Your agent's skills don't break. They expire.

A maintenance layer for agent skills.
Your agent's skills don't break. They expire.

The layer does four things. It enforces a fixed structure on every skill file at the moment you commit one. It reads those files against reality every night and reports what no longer matches. It demands a failing test before any instruction changes. And it routes your offhand replies back into the skill that produced the output.

The problem it solves is that instructions for agents are prose, and prose does not break. When a script rots, a build turns red. When a procedure rots, the agent executes it competently, produces something plausible, and nothing anywhere goes red. Nine repairs to my own setup in a single day: one was a bug, the other eight were instructions that had stopped being true. A skill posting to a channel I had renamed. A file pointing at a script I had deleted. A procedure that made perfect sense in April and matched nothing by June.

I did not find those eight by reading my instruction files back. I have never read them back, and nothing in my setup was reading them either.

GitHub - snamrick/groundskeeper: A maintenance layer for agent skills: structure enforced at commit time, expiry detected nightly, changes that carry proof
A maintenance layer for agent skills: structure enforced at commit time, expiry detected nightly, changes that carry proof - snamrick/groundskeeper

Plausible is the dangerous part

A skill is a markdown file: a written procedure the agent follows for a recurring job. Morning briefings, weekly reviews, newsletter drafts, change requests against its own instructions. I run about thirty-five of them. Yours might be slash commands, system prompts, a folder of "how we do things" documents. Same material: promises written in prose. Mine run in Claude Code, which loads them from a folder of markdown files. Nothing in the layer depends on that runtime - it reads and checks markdown - but that is what it was built and measured against.

Plausible is the dangerous part. A hard crash gets investigated the same day; a briefing that silently dropped one section because its data source moved gets read, half-noticed, and forgiven.

One of my skill files once sat modified-but-uncommitted for six days. Not broken, not flagged, just drifting outside version control while every nightly process reported green, because none of them considered "text nobody is watching" to be a problem.

The rule: if no process reads an instruction file against reality, assume it has already diverged.

Enforcement you don't have to remember

The first piece of the layer is a template with teeth. Every skill has the same seven sections: trigger, inputs, pre-flight checks, steps, outputs, verification, and what it is not for. That last one matters more than it looks: a procedure that cannot say what it is not for will slowly absorb neighbouring jobs until nobody knows what invokes what.

A pre-commit hook enforces this: a skill missing a section does not get committed. It requires the skill folder to be a git repository - step zero, and worth more than the hook itself.

My gate lied to me for months. A sandboxed environment blocked one small operation inside the hook, the hook swallowed the error, found its own error file empty, and returned success. Every commit passed. The gate existed, was installed, ran on every commit, and validated nothing. I only found out when I tested the gate the way you would test a suspect: by feeding it a commit that deserved to fail, and watching it approve.

That incident set the rule that now colours every check in the system: a check has three possible outcomes, not two. OK means it ran and found nothing. FAIL means it ran and found something. UNKNOWN means it could not run, and UNKNOWN is never, ever allowed to look like OK. Most home-grown checks collapse those last two into silence. That silence is where the months of false green came from.

The rule: enforce structure at the moment of change, and give every check three outcomes. A check that cannot run must say so.

A nightly job reads your instructions so you don't have to

Enforcement catches bad changes at the moment you make them. Expiry is the opposite problem: the text stayed the same and the world moved. No commit will ever trigger, because nothing was committed. So the second piece is a nightly job that reads the instruction files against reality and reports three kinds of decay.

References to steps that no longer exist: my notes and memory files point into skills by step name, and when a skill gets renumbered, the pointers go stale. Paths that no longer resolve: a skill that says "read this file" where the file has moved. And edits that never made it into version control, the six-days-dirty case from earlier.

Two design choices did most of the work here. First, expiry warns and never blocks. A stale reference is a cleanup signal, not an emergency, and a nightly job that can break your morning routine gets disabled within a week. Tolerated findings live in a committed snapshot file, so the job only ever reports what is new. Silence stays meaningful.

Second: a living document may not contain outcomes, only the commands that produce them. My own architecture doc once promised "18 tests passing" while the suite ran 108. Nobody updates a number in prose. So now a test literally forbids volatile numbers in that document; every claim must be written as the command you would run to check it.

The nightly report is a short list of lines like these, all from my own logs. Skill X points at a script that no longer exists. Skill Y was modified six days ago and never committed. Document Z claims "18 passed"; the suite runs 108. Nothing on that list is an error your agent would ever raise. Each line is a fact with the command that proves it, and an empty report means something, because tolerated findings live in a committed snapshot and only news gets reported.

The rule: expiry warns, never blocks. Tolerated findings live in a committed snapshot so only news reports. Living documents carry commands, not outcomes.

Your feedback becomes change requests with proof attached

I have never filed a ticket against my own agent. What I do is respond, briefly and in passing: "too long", "wrong channel", "good, but move it to Tuesday". Most of those responses tune the next run directly. But some of them point at the instructions themselves, and that is the third piece: a change request, and a change request must carry a test. Specifically: the command that fails before the change and passes after it. Red, then green.

My briefings kept using markdown tables, which my chat client renders as a mess of pipe characters. The complaint became a test that greps the next briefing for pipes. It failed. The instruction changed, it passed, and it has quietly guarded every briefing since. That is the difference between an edit and an improvement.

Without a failing test in hand, every "improvement" to an instruction file is a vibe. I reword a step, it reads better, and I have no idea whether anything the agent does will differ. With the red/green demand, half of my proposed changes die before they are made, because I cannot produce a command that fails. That is the system telling me the problem I was about to fix does not exist.

New skills face the same demand at birth. Before a skill may exist, one representative task runs on a fresh agent without it. If the agent manages fine, the skill does not get built. This kills more ideas than anything else in the layer. Writing new instructions is also a form of pretending. A skill without a demonstrable failure case is not automation, it is a feeling of being organised, filed under a slash command.

The rule: no change without the command that fails before it and passes after. No new skill without a recorded failure without it.

A reply is the cheapest change request

Everything above assumes the maintenance signal exists somewhere. The most valuable signals are the ones I give off without noticing: offhand replies to the agent's output. The fourth piece harvests those.

When a skill posts its result to a chat thread, it registers a small protocol alongside: what to do when a reply lands here. A "too long" gets parsed, lands in that skill's feedback log, and the next run reads the log before it writes a word. No ticket, no form, nothing to remember. The system is built on the assumption that I will never do more than react.

The channel itself only needs one property: threads with stable identities, so a reply can be traced to the skill and the run that produced it. Anything with that property can carry the loop. Anything without it, a plain webhook, a notification service, can still deliver output, but it can never learn. Delivery without a return path is a megaphone, not a loop.

The rule: deliver where replies can find their way back. A channel without a return path is output, not input.

What it still cannot do

The expiry detector catches names, not meaning. "Step 11 does X" goes stale the moment step 11 does Y instead, and the detector stays green as long as a step 11 exists. Semantic drift is caught by exactly one mechanism: a human reading, occasionally, with attention. No nightly job replaces that.

Distilling a reply into the right change is still judgement. The loop delivers your complaint to the right doorstep with context attached; deciding what the complaint means is not automated, and I distrust versions of this that claim otherwise.

Coverage is partial and stays partial. Not every skill has a feedback log. Not every job is gated. The honest version of the claim is not "this system maintains itself" but "this system knows where it is unmaintained and says so out loud", which turns out to be most of the value.

The documents describing this layer expired too. The "18 tests passing" doc was the doc explaining expiry. A layer that promises more than it delivers is worse than no layer at all, because it teaches you to stop looking. Every guarantee in this essay comes with its edges stated for that reason.

The groundskeeper

People who build systems like this like to think of themselves as gardeners: growing ideas, connecting things, tending a plot. It is a flattering self-image, and it skips the one job every garden depends on: somebody has to mow the lawn.

That is the piece I ended up building, and the name I have settled on for it: a groundskeeper. Not the gardener, that is you, on your good days. The groundskeeper is the hired layer that maintains the grounds whether or not the owner shows up, and assumes the owner will not.

The repo is called groundskeeper, and it contains the layer, not my garden: the skill template with its enforcement hook, the three-outcome verification, the nightly expiry auditor, the change-request format with red/green proof, and the channel adapter with a file-based demo so you can run the whole loop without any particular chat platform. The limits are stated, because the limits section is the part I would want from anyone else's system. On Claude Code the repository doubles as a plugin marketplace, so installing it is two lines and a skill that sets it up where you are; anywhere else it is a git clone and one script.

One design choice worth stating out loud: the repo is generated from my live system, and the two stay separate worlds. A pull request there can teach my setup something, and an improvement here can refresh the repo, but neither holds the other hostage. That is not a shortcut, it is the same expiry-thinking applied to the repo itself: a public promise of eternal maintenance would be one more claim that expires without saying so.

What I want from you is narrower than praise: tell me which part you would run, because that decides what gets attention first. James Carse wrote that gardens do not die in winter; they quietly prepare for another season. Skills are the same. They are never finished, only tended. The groundskeeper is how the tending gets done.

GitHub - snamrick/groundskeeper: A maintenance layer for agent skills: structure enforced at commit time, expiry detected nightly, changes that carry proof
A maintenance layer for agent skills: structure enforced at commit time, expiry detected nightly, changes that carry proof - snamrick/groundskeeper