Make every skill, hook, and scheduled job declare four invariants before it ships — Locality (where it can run), Source-of-truth (which facts it owns or borrows), Cross-ref (what depends on it and what it depends on), and Trigger-measurability (whether its trigger is observable at runtime or hidden in external state) — and refuse to hand off any component that leaves one undeclared, because an undeclared assumption is exactly the seam where two components silently disagree.
When you build an AI agent system out of small parts — skills the model loads on demand, hooks that fire on lifecycle events, cron jobs and scheduled routines that run unattended, helper scripts, config profiles — each part usually gets tested in isolation. It works. You move on. The trouble is that "it works" only proves single-shot correctness; it says nothing about whether the part's assumptions agree with the rest of the system.
Every component carries hidden assumptions: where it runs (local machine vs. a remote sandbox), which facts it treats as authoritative, what other components it silently depends on, and what its trigger actually measures. When those assumptions go undeclared, conflicts stay invisible until they surface to the user as a flaky, hard-to-trace symptom — the kind that feels like a vicious cycle because every fix in one place re-opens a gap somewhere else. The deeper problem isn't any single line of code; it's that each component is operating without knowing its own assumptions, so nothing can detect when two of them disagree.
The rule is simple and enforceable: every new skill, hook, cron job, script, or routine declares four invariants, or it cannot be handed off. An undeclared invariant means the component itself is incomplete — it is the literal root of conflicts, leaks, and automations that bill compute but return nothing.
local-only, repo-only, or both. A job that runs in a remote sandbox but references local-only paths is broken before it starts.sources: [...] list of the files/components it reads.consumers: [...] list (who applies or reads it) and a "See also" section (what it relies on).These four answer the four questions that, left unanswered, become the four ways components leak into each other.
Leak 1 — stale source-of-truth. A small script reported which config profile was active by reading the mcpServers key from one settings file. But the authoritative definition of those servers had migrated to a different file. The script was written when the first file was the source of truth, and never updated after the move — so it always returned the default base, and the status it displayed was quietly wrong. Had a Source-of-truth invariant been declared ("this fact lives in file B, not file A"), the mismatch would have been visible the day the truth moved.
Leak 2 — locality mismatch. A scheduled routine was registered to run in a remote sandbox, but its prompt referenced local-only resources (paths that exist only on the developer's machine). This was caught in the final seconds before registration. Any later and the schedule would have fired on the remote, found nothing, and produced billed-but-empty runs on a recurring cadence. A declared Locality invariant ("this runs remote → it may only reference remote-visible resources") would have flagged it immediately.
The two leaks looked unrelated — one a config-reading bug, one a scheduling mistake — but they share a single cause: a component that never declared its assumptions. That they happened in the same session is the tell that this is a structural gap, not a one-off slip.
Keep the declaration cost to roughly one line per invariant — the weight of declaring is far below the cost of verifying a conflict after the fact. Concretely:
# For a reusable decision record / doc with frontmatter:
---
id: NOTE-XXX
locality: local-only # or repo-only / both
sources: ["path/to/file-that-owns-the-fact"]
consumers: ["who-applies-this", "downstream-automation"]
---For a skill or command, put the same four in the markdown header or description — minimally a one-liner like (locality: local-only — depends on ~/config) plus a "See also" listing the RDUs/skills/commands it relies on. For a hook defined in JSON (where comments aren't allowed), register the invariants in the hook's companion doc and list the settings file in sources. For a cron job or routine, gate registration behind five questions answered out loud first: (1) Locality — does it run local or remote, and do its referenced resources exist there? (2) Sources — what's the source-of-truth for every fact the prompt cites? (3) Cross-ref — where does the output flow (a user report, a file update, a record evolution)? (4) Trigger-measurability — does the cron time line up with resource availability (machine on, service reachable)? (5) Reversibility — how do you turn it off if it's wrong (launchctl unload / disable / delete)? If even one answer is fuzzy, hold registration until it's clear.
You don't have to retrofit everything at once. Prioritize by blast radius. System entry points (the scripts that classify profiles or route config) come first — they're the most dangerous because everything downstream trusts them. Next, hooks that fire on every prompt, since a wrong assumption there taxes every interaction. Then your highest-frequency skills. Everything else can be backfilled slowly on a periodic audit cadence.
Bake the check into that cadence: scan for decision records whose frontmatter is missing locality (backfill candidates), skills whose headers don't declare invariants (backfill candidates), and any routine/cron/hook unchanged for 30+ days (re-verify its invariants, because the world it assumed may have moved underneath it — exactly how Leak 1 happened). Declaring an invariant isn't bureaucracy; it makes each component self-document its assumptions so that the next person to touch the system — including a future version of you, or your own agent — can detect a conflict on sight instead of debugging it in production.
This is a discipline, not a compiler. Nothing mechanically enforces the four invariants unless you build that enforcement — a frontmatter linter, a registration gate, a pre-commit check. Declarations can also drift: a component can declare sources: [file A] truthfully on Monday and become a lie on Friday when the source-of-truth moves, which is precisely why the 30-day re-verification step exists rather than a one-time declaration. The invariants also can't catch a conflict between two components that each declare correctly but make incompatible assumptions about each other's behavior — declaration surfaces the seam, it doesn't reconcile semantics. And there's a real failure mode of over-declaring: if every trivial helper grows a four-field header, people stop reading them and the signal drowns. Keep declarations to one line each, reserve the full five-question gate for unattended automation (where a silent failure is most expensive), and treat the lightweight inline form as enough for in-session skills and commands.
Q. My agent's scheduled job runs fine when I trigger it manually but produces nothing on its cron schedule. Why?
This is almost always a Locality mismatch: the manual run executes in your local environment where the prompt's referenced resources (file paths, local scripts, machine-only config) exist, but the scheduled run executes somewhere else — a remote sandbox or a different user context — where those resources don't exist. The job fires, finds nothing, and returns empty. Before registering any scheduled automation, declare its Locality (local-only / repo-only / both) and verify that every resource the prompt references is actually visible in the environment where it will run. If it runs remote, it may only reference remote-visible resources.
Q. A status command in my agent keeps reporting stale or wrong configuration. The code looks correct. What's going on?
Check whether the fact it reads has moved. A very common pattern: a component reads a value from file A because file A used to be the source of truth, but the authoritative definition later migrated to file B, and the reader was never updated. The code is internally correct — it's just reading the wrong place. Fix it by declaring a Source-of-truth invariant for the component (a sources: [...] list naming where each fact actually lives) and adding a periodic re-verification step, so when a source-of-truth moves you have a checkpoint that catches the now-stale reader instead of letting it report wrong values silently.
Q. What exactly are the four invariants and what question does each one answer?
Locality answers "where can this run?" (local-only, repo-only, or both). Source-of-truth answers "which facts does it own authoritatively, and where do borrowed facts live?" (a sources list). Cross-ref answers "what depends on this, and what does it depend on?" (a consumers list plus a See-also section). Trigger-measurability answers "can the trigger be observed at runtime inside a session, or does it depend on external state like the machine being on or a service being reachable?" Each unanswered question is one of the four ways components silently leak into each other, so declaring all four closes the seams up front.
Q. Do I really need to add four metadata fields to every tiny helper script?
No — match the declaration weight to the component's blast radius. For unattended automation (cron jobs, scheduled routines) where a silent failure is most expensive, run the full registration gate: answer Locality, Sources, Cross-ref, Trigger-measurability, plus Reversibility (how to turn it off) before registering. For in-session skills and commands, a single inline line is enough, e.g. (locality: local-only — depends on ~/config) plus a See-also list. The principle is that the cost of declaring should stay far below the cost of debugging a cross-component conflict — keep it to one line per invariant so the signal doesn't drown.
Q. How do I retrofit invariants onto an agent system I already built?
Prioritize by blast radius rather than trying to do it all at once. First, system entry points — the scripts that route config or classify profiles — because everything downstream trusts them. Second, hooks that fire on every prompt. Third, your highest-frequency skills. Everything else gets backfilled on a recurring audit cadence. In that cadence, scan for: decision records missing a locality field, skills with no declared invariants, and any routine/hook unchanged for 30+ days (re-verify these, because the environment they assumed may have shifted). The 30-day re-check is what catches source-of-truth drift, which is one of the two leaks this pattern was built to prevent.