Agent skill

Declaring Invariants

by oaustegard in oaustegard/claude-skills

Find tests that enumerate a domain by copying it, and declare the invariants a codebase depends on.

MITAuto-check passed

Install Declaring Invariants

skills CLI
$ npx skills add oaustegard/claude-skills --skill declaring-invariants -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install oaustegard/claude-skills declaring-invariants --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/oaustegard/claude-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/declaring-invariants .claude/skills/declaring-invariants && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
declaring-invariants
GitHub stars
150
Token cost
~3.6k tokens
SKILL.md length
1,915 words
Files
7 (incl. scripts)
Skills in repo
67
Repo updated
First seen
Licence
MIT

At a glance

Find tests that enumerate a domain by copying it, and declare the invariants a codebase depends on.

  • Works in 5 steps: Break the chokepoint the claim names,… → Run the claiming test. Read the failure… → Run the rest of the file. Confirm the… → …
  • Reviewing tests
  • SKILL.md covers The failure it catches, totality_lint.py — tests that…, Why a hand-list is the second… and claims.py — what the repo…, plus 5 more sections
  • Runs Python scripts from its folder; calls python3

What it does

Declaring Invariants is an agent skill from oaustegard/claude-skills. Find tests that enumerate a domain by copying it, and declare the invariants a codebase depends on. Reports where a parametrize list, for-loop, or it.each iterates a hand-written subset of a dict/set/tuple/Enum that exists in the source, and names the members nothing covers. Use when reviewing tests, when a module gains a name-to-thing table, registry, enum, or dispatch map, before trusting a green suite as evidence a domain is covered, or when asked "is this test actually total", "does anything cover X", "what…

Its SKILL.md is about 3.6k tokens, which your agent loads only when the skill is triggered. The skill folder holds 8 other files, including scripts (for example `CHANGELOG.md`, `README.md` and `scripts/claims.py`).

The repository describes itself as: My collection of Claude skills. The licence is MIT.

When your agent uses it

  • Reviewing tests
  • A module gains a name-to-thing table
  • Before trusting a green suite as evidence a domain is covered
  • Asked is this test actually total

Example prompts

  • “is this test actually total”
  • “does anything cover X”
  • “what does this repo guarantee”
  • “/declaring-invariants”

Requirements

  • Python 3

Workflow steps

5 steps, taken from the first numbered list in SKILL.md.

  1. Break the chokepoint the claim names, with one edit in the source rather
  2. Run the claiming test. Read the failure text. Note the member it named.
  3. Run the rest of the file. Confirm the others stay green — if everything goes
  4. Restore the source.
  5. Write refuted: -> .

What it can do on your machine

Read from SKILL.md and the folder at commit 90b0f1b. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Ships 2 files in scripts/ (Python), which the agent can run.

    Shell commands in SKILL.md call:

    • python3

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Links to these hosts (documentation or services it may open):

    • github.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Declaring Invariants loads about 3.6k tokens when it runs. Until then it costs about 199 tokens; SKILL.md has 1,915 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~199
When it runs · the whole SKILL.md, loaded when a task matches
~3.6k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.

SKILL.md

The full file from oaustegard/claude-skills at commit 90b0f1b, republished under its MIT licence (© oaustegard). 1,915 words, ~3,626 tokens.

Download SKILL.mdSave it as .claude/skills/declaring-invariants/SKILL.md (or your agent's skills folder). This skill also uses 6 other files; get the full folder from GitHub.
name
declaring-invariants
description
Find tests that enumerate a domain by copying it, and declare the invariants a codebase depends on. Reports where a parametrize list, for-loop, or it.each iterates a hand-written subset of a dict/set/tuple/Enum that exists in the source, and names the members nothing covers. Use when reviewing tests, when a module gains a name-to-thing table, registry, enum, or dispatch map, before trusting a green suite as evidence a domain is covered, or when asked "is this test actually total", "does anything cover X", "what does this repo guarantee", "which invariants do we declare". Also for vacuous tests that pass over an empty collection, for a domain that has silently NARROWED (an enumeration cannot see that), and for recording the refutation that proves a claim can fail.
metadata.version
0.2.3

declaring-invariants

Two scripts over one idea: a test that enumerates a domain must loop the registry rather than a copy of it. Someone also has to say which domains matter in the first place.

bash
python3 scripts/totality_lint.py <repo>   # tests that copy a domain
python3 scripts/claims.py <repo>          # what the repo declares, and what backs it

Python only, stdlib ast only: no install, no config file, no network.

The failure it catches

A test that loops a hand-written list passes its runner and proves nothing about completeness. When the same members also exist as a registry in the source, the list is a copy, and the copy drifts the moment someone adds a member to the registry and not to the test. Nothing goes red.

Measured on oaustegard/remex, 2026-08-24: adding a fourth member to ROTATION_CODES with no construction behind it left the entire 267-test suite green. Four separate tests looked total; each parametrized ["haar", "rht"] against a three-member registry. Only a test that looped the registry itself caught it.

Adapted from the meta-oracle in daniloc/coherence (src/oracle-domain.ts), which classifies an oracle's iteration root as LIVE or LITERAL by parsing the oracle's own AST. That harness needs spec files, a claim grammar, a ledger and Node; the check does not.

totality_lint.py — tests that copy a domain

findingmeaning
sampled-domaina parametrize or for over a literal whose members are a strict subset of a source registry. The uncovered members are named.
ratchet-brokena hand-list marked ratchet names a member the registry no longer contains. Detected statically, without running anything.
unratcheteda registry enumerated live with nothing pinning its membership. Suppressed when no-floor already claimed the same line.
no-floora test iterates a live registry with no len(...) >= n assertion in the file, so an emptied registry passes vacuously.
stale-ackan acknowledgement on a test that now covers the whole domain.
bash
python3 scripts/totality_lint.py <repo>            # the report
python3 scripts/totality_lint.py <repo> --json
python3 scripts/totality_lint.py <repo> --strict   # exit 1 if any finding
python3 scripts/totality_lint.py --selftest        # fixtures, no repo

A partial domain is often correct. Say so on the test and it stops being a finding:

python
# totality: partial — mojo has no construction for "none"
@pytest.mark.parametrize("rotation", ["haar", "rht"])
def test_save_params_accepts_every_mojo_rotation(rotation): ...

The marker also works as totality: partial — <why> inside the docstring. An acknowledgement on a test that later covers the whole domain is reported as stale-ack, so a suppression cannot become a silence.

Why a hand-list is the second form

An enumeration loops whatever the domain currently holds, so it is structurally blind to the domain narrowing. Remove a member and the loop simply ranges over fewer of them, green. Raised by Yep, 2026-08-24; reproduced on oaustegard/remex before being believed:

perturbation of ROTATION_CODESdomain floorenumerationparityhand-list
grow — add "hadamard2"passREDREDpass
shrink — drop "none"REDpassRED*pass
substitute — "none" → "xyz" in both spellingspasspasspasspass

* only because the other spelling of the domain did not shrink, which is incidental to that repository.

In the substitution row cardinality holds, both spellings agree, and five green tests cover a registry that quietly stopped supporting a rotation every index on disk was written with. The floor is a cardinality check; it cannot see a member swapped for another.

The second form is a ratchet: a hand-list asserting the domain keeps containing it. Mark it and the linter checks the pin rather than taking the marker's word for it.

python
# totality: ratchet — these three shipped; one leaving is a compatibility break
def test_no_shipped_rotation_is_ever_removed():
    for shipped in ("haar", "rht", "none"):
        assert shipped in ROTATION_CODES

Under that same substitution the linter reports ratchet-broken naming 'none' statically, before any test runs, and the test itself goes red while the other five pass.

So: an enumeration proves every current member is handled, and a ratchet proves no member left without a decision. unratcheted names a registry that has the first and not the second. Neither half alone is the answer.

A ratchet covers shrink only. Over a strict subset it still reports sampled-domain for the members it never listed, because pinning two of three says nothing about the third. Suppressing that would make the marker a laundering channel — the escape hatch these notes criticise via guard for being in coherence. It was one: until 2026-08-25, # totality: ratchet over two of three members silenced the report entirely and checked nothing about the third. Caught by an adversarial pass on this skill, not by its own tests.

claims.py — what the repo declares

totality_lint asks whether a test's domain is complete. It presumes a test exists. This asks the prior question, the one coherence's own Known Limits concedes it does not answer: nothing enforces exists ⇒ declared.

A claim is a test whose docstring opens with invariant:. No new file format, and the claim inherits its test's pass/fail:

python
def test_every_skipped_directory_is_actually_skipped(self):
    """invariant: every name in SKIP_DIRS is excluded from the walk.

    refuted: replaced the walk's `part in SKIP_DIRS` check with
    `part in {"node_modules"}` -> this test went red naming `.coherence`,
    while the other 25 tests in this file stayed green.
    """
findingmeaning
unrefuteda claim nobody has watched fail
literalthe claiming test iterates a copy of the registry, so the claim cannot see a new member
unanchoreda registry no invariant names — a question, not a verdict

A claim whose test carries a ratchet marker is reported as pinning its registries rather than copying them, and a ratcheted registry is not unanchored.

bash
python3 scripts/claims.py <repo> [--json] [--strict] [--selftest]

Write the refutation from what you observed

refuted: is the half that costs something. Break the chokepoint, watch the claim go red by name, restore, and record what you saw. A green test and an unfalsifiable one look identical from outside; the refutation is what separates them.

Never write a refutation you have not run. The first refutation authored for the SKIP_DIRS invariant above asserted a failure that did not occur — the fixture placed the registry outside the test's reachability, so the test passed under perturbation for an unrelated reason. A vacuous claim, written while building the tool that catches vacuous claims, and caught only by running the perturbation instead of trusting the sentence.

Procedure, in order:

  1. Break the chokepoint the claim names, with one edit in the source rather than in the test.
  2. Run the claiming test. Read the failure text. Note the member it named.
  3. Run the rest of the file. Confirm the others stay green — if everything goes red, the claim is not localised and the refutation says nothing.
  4. Restore the source.
  5. Write refuted: <the edit> -> <what went red, by name>.

Wiring it into a commit gate

The reference wiring lives in oaustegard/claude-workspace (scripts/tdd_hook.py): a commit where a registry gained a member and no invariant: test iterates it live is denied, naming the registry and what it gained. Override with no-invariant: <why> in the commit body.

Registry shrink is gated the same way, and needs a ratchet rather than an enumeration to clear it. A new function or a new branch is behavioural growth too, but neither is diffable without guessing, and a gate that guesses stops being consulted. A brand-new registry is not gated. Declaring one is a judgement call; growing one has already made it.

Where the two filters came from

Both filters exist because the unfiltered version was noise. Reproduce either by removing the filter and re-running against a real repo.

  • no-floor fires only on a name independently recognised as a registry. Firing on every for x in <local> produced 27 findings on remex, all noise: numpy arrays, query matrices, loop counters.
  • A literal matches a registry only when reachable — the test imports its module or package, shares its top-level directory, or is its paired tests/test_<mod>.py. Matching against any registry in the tree joined a test in discrepancy/ to registries in kb-k-sweep/ and remex-vs-higgs-ablation/ on a monorepo, because small integer sets collide by chance. Four findings became one, and the survivor was real.

The join key is membership, not names: [1, 2, 3, 4, 8] and SUPPORTED_BITS share no token, so containment is what ties them together. No naming convention is assumed, and none is required.

Show full SKILL.md (693 more words)Show less

Limits

  • Python only. The extractors are _registries_py and _domains_py; a tree-sitter pair for another language slots in beside them. The join, the acknowledgements and the reports are all language-independent.
  • Report, not gate, by default. --strict opts into a nonzero exit. The tool this was adapted from gates by default, and its parity arm false-fails a correct oracle that binds its domain to a local name first. A gate that false-fails stops being consulted.
  • A registry is a collection of constants. A dict/set/tuple/list of literals, or an Enum body, with at least three members. A domain assembled at runtime is invisible here.
  • Multi-parameter tables are out of scope. Only single-name parametrize is read.
  • unanchored is a question. Most registries need no invariant. Treat the list as candidates for declaration, never as a backlog to clear.
  • A registry is modelled as a SET OF KEYS, so a value swap is invisible. This is the largest hole in the design, and it undercuts the motivating example. ROTATION_CODES = {"haar": 0, "rht": 1, "none": 2} exists to pin bytes on disk; permuting it to {"haar": 0, "rht": 2, "none": 1} keeps every key, every count and every ratchet intact, and every index already written decodes under the wrong rotation. Verified 2026-08-25 on a fixture: the linter reports nothing and the gate's registry half reports nothing. (The gate denied that fixture, but on the unrelated TDD rule — checked, because claiming otherwise would have been the overclaim this skill exists to catch.) Nothing here checks a name-to-code mapping. Extracting (key, value) pairs as the member set would, at the cost of breaking the subset join against a parametrize list of keys.
  • A co-ordinated rename defeats the ratchet. A global find/replace that renames a member in the registry AND in the hand-list leaves the pin intact, and this reports nothing. Verified 2026-08-25 on a fixture: renaming "none" to "identity" in both files was silent, while every index already on disk still decodes byte 2 as the old name. Both sides of a co-located hand-list move together, so no static check over one working tree can see it. The commit gate can, because it diffs against git history — and only if the ratchet itself is untouched in that commit, which it now requires.
  • A brand-new registry is neither gated nor pinned. Create one with five members and never grow it and no gate ever fires. unanchored and unratcheted surface it in the report; the gate deliberately does not, on the grounds that gating every new module is how a gate stops being consulted. That is a judgement, not a proof, and it is the largest hole a cross-model review found.
  • A decorator-built registry is invisible. @register("name") populating a dict at import time is the common Python registry idiom and is not a literal, so nothing here sees it. Named because it is the shape most likely to be mistaken for coverage.
  • Reachability is path-based, so a disconnected integration test is missed. A test that neither imports the module, shares its top-level directory, nor pairs with it by filename will not be joined to the registry it samples. The filter trades that recall for the cross-project precision it was measured to buy.
  • The precision numbers come from three repositories, and the filters were fitted to two of them. 27 noise findings on one, 3-of-4 cross-project joins on another. Those are the measurements that justified each filter; they are not a false-positive rate on a corpus, and should not be read as one.
  • Split and merge are invisible. The diff is keyed on registry NAME, so renaming a registry, splitting one in two, or merging two into one falls through both the gained and lost paths.
  • A claim that passes is not a claim that is right. This checks that a declared invariant loops the domain it names. Whether it is the right invariant is human judgement, and it is not automatable.
  • verifying-claims covers the prose layer: does the documentation match reality? Agent-judged, non-deterministic, run as a triggered review. This skill is the deterministic half, over code and tests rather than prose.
  • tree-sitting locates the registry or the test before you edit it.

© oaustegard, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 6 other files (scripts) in declaring-invariants of oaustegard/claude-skills.

  • SKILL.md
  • CHANGELOG.md
  • README.md
  • scripts/claims.py
  • scripts/totality_lint.py
  • tests/test_claims.py
  • tests/test_totality_lint.py

Open the folder on GitHubat commit 90b0f1b

Compare with similar skills

Declaring Invariants next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Declaring Invariants compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Declaring Invariants this skilloaustegard/claude-skills150—~3.6kAutomated safety check: PassMIT
Copy Editingcoreyhaines31/marketingskills54k1 repos~4.5kAutomated safety check: PassMIT
Sitemap Domainthedaviddias/Front-End-Checklist74k—~531Automated safety check: PassMIT
Domain Driven Designdavila7/claude-code-templates33k5 repos~623Automated safety check: PassMIT
Domain Modeling and Glossarywindmill-labs/windmill18k—~622Automated safety check: PassCustom licence
Invariant Guardsickn33/agentic-awesome-skills47k1 repos~4.5kAutomated safety check: PassApache-2.0

Similar skills

  • Copy Editing

    coreyhaines31/marketingskills

    When the user wants to edit, review, or improve existing marketing copy, or refresh outdated content.

    54k GitHub starsUsed in 1 repo~4.5k tokens
    Writing & ContentAuto-check passed
  • Sitemap Domain

    thedaviddias/Front-End-Checklist

    A skill your agent uses when applies to sites that have recently migrated from HTTP to HTTPS, changed domain name, or have www/non-www redirect configurations.

    74k GitHub stars~531 tokensUpdated 4 days ago
    Marketing & SEOAuto-check passed
  • Domain Driven Design

    davila7/claude-code-templates

    Plan and route Domain-Driven Design work from strategic modeling to tactical implementation and evented architecture patterns.

    33k GitHub starsUsed in 5 repos~623 tokens
    DevelopmentAuto-check passed
  • Domain Modeling and Glossary

    windmill-labs/windmill

    Actively challenges vague or conflicting terminology as you design, and keeps a living domain glossary file up to date in real time.

    18k GitHub stars~622 tokensUpdated today
    DevelopmentAuto-check passed
  • Invariant Guard

    sickn33/agentic-awesome-skills

    Correctness-first: forces writing the function contract, loop invariant, termination argument, and edge cases BEFORE code.

    47k GitHub starsUsed in 1 repo~4.5k tokens
    Auto-check passed
  • Declarative Agents

    github/awesome-copilot

    Official

    Complete development kit for Microsoft 365 Copilot declarative agents with three comprehensive workflows (basic, advanced, validation), TypeSpec support, and Microsoft 365 Agents Toolkit integration

    40k GitHub starsUsed in 1 repo~1.2k tokens
    Documents & OfficeAuto-check passed

More from oaustegard/claude-skills

All 67 skills in this repo
  • Vega-Lite Interactive Charts

    oaustegard/claude-skills

    Builds interactive Vega-Lite charts from uploaded data: analyzes the fields, picks five to ten fitting chart types, and produces a React artifact with the data embedded inline.

    150 GitHub stars~2.1k tokensUpdated today
    Auto-check passed
  • Single-File HTML Composer

    oaustegard/claude-skills

    Builds self-contained single-file HTML pages such as reports, decks, postmortems, flowcharts and prototypes from a small spec using a bundled Python composer and templates.

    150 GitHub stars~3.2k tokensUpdated today
    Auto-check passed
  • Deciding With Confidence

    oaustegard/claude-skills

    Routes, triages, flags and rates a piece of text with a probability for every option: which department or queue a ticket goes to, which intent a message expresses, whether a yes/no condition holds…

    150 GitHub stars~2.6k tokensUpdated today
    Auto-check passed
  • Declauding

    oaustegard/claude-skills

    Rewrites model-sounding prose into plain technical writing and checks that every claim survives, for PR text, docs, commit messages and similar drafts.

    150 GitHub stars~5.2k tokensUpdated today
    Auto-check passed
  • Preact Developer

    oaustegard/claude-skills

    Guides building standards-based Preact apps with native-first choices, HTM syntax, import maps and vendored ESM, from single-file demos to larger builds.

    150 GitHub stars~4.6k tokensUpdated today
    Auto-check passed
  • Bluesky Zeitgeist Sampler

    oaustegard/claude-skills

    Deprecated sampler that captures short windows of the Bluesky firehose, clusters trending terms and builds an HTML report; replaced by the browsing-bluesky skill.

    150 GitHub stars~1.4k tokensUpdated today
    Auto-check passed

Questions about Declaring Invariants

What does Declaring Invariants do?

Find tests that enumerate a domain by copying it, and declare the invariants a codebase depends on. Declaring Invariants is an agent skill from oaustegard/claude-skills. Find tests that enumerate a domain by copying it, and declare the invariants a codebase depends on.

When should I use Declaring Invariants?

Declaring Invariants fits situations like: reviewing tests; A module gains a name-to-thing table; before trusting a green suite as evidence a domain is covered; asked is this test actually total.

How do I install Declaring Invariants in Claude Code?

Run `npx skills add oaustegard/claude-skills --skill declaring-invariants -a claude-code`. Or copy the skill folder (declaring-invariants in oaustegard/claude-skills) into .claude/skills/declaring-invariants in your project. Claude Code loads it when a task matches its description.

How do I install Declaring Invariants in Codex?

Run `npx skills add oaustegard/claude-skills --skill declaring-invariants -a codex`. Or copy the skill folder (declaring-invariants in oaustegard/claude-skills) into .agents/skills/declaring-invariants in your project. Codex loads it when a task matches its description.

Can I use Declaring Invariants in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add oaustegard/claude-skills --skill declaring-invariants -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/declaring-invariants, .gemini/skills/declaring-invariants, .github/skills/declaring-invariants and .opencode/skills/declaring-invariants in your project.

What does Declaring Invariants need to run?

Going by SKILL.md and its folder, Declaring Invariants needs Python for the scripts in its folder and the command-line tools its instructions call (python3). Our summary lists: Python 3.

Does Declaring Invariants access the network?

SKILL.md names 1 domain. As links in the text: github.com. This is read from the text; nothing was executed.

Is Declaring Invariants safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.

What licence does Declaring Invariants use?

Declaring Invariants is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Declaring Invariants use?

About 3.6k tokens (SKILL.md is roughly 15k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Declaring Invariants?

Skills that share tags, products or a category with Declaring Invariants: Copy Editing (coreyhaines31/marketingskills, 54k stars), Sitemap Domain (thedaviddias/Front-End-Checklist, 74k stars), Domain Driven Design (davila7/claude-code-templates, 33k stars) and Domain Modeling and Glossary (windmill-labs/windmill, 18k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Declaring Invariants?

oaustegard (a GitHub user) maintains it in oaustegard/claude-skills, which has 150 GitHub stars. The repository holds 67 skills in this directory. The repository was last updated on October 9, 2026.

Source: oaustegard/claude-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.