Skip to content

About

Repository rules an agent cannot forget: defeasible rules over clingo and s(CASP). Every answer is three-valued and carries the full circle of rules that bore on it, so a conclusion drawn from an incomplete reading becomes detectable.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

agent-rule-corpus

English · Русский

Repository rules that are all held at once — unlike an agent, whose rules fall out of context in a long session.

A question is asked before the action. The answer is three-valued, and it comes with the circle of consideration: which rules bore on the case, and what happened to each.

Status: proof of concept. The approach is not new — start with docs/prior-art.md, which lists honestly who did this first and how this assembly differs.

Two numbers before anything else. Of 21 norms in a real Go style guide — Thanos's, minus what its own authors file under "enforced by linters" — one is expressible in this fact vocabulary. Of 52 norms in prometheus/prometheus's governing documents, the process-and-authority domain this was actually built for, also one.

The engine is correct. The extractor is pointed at the wrong object: every fact here describes a file at rest, and every norm worth enforcing is about a change, an actor, or process state. See docs/approach.md.

Read docs/known-defects.md before trusting anything here. An agent was told to break this repository and did: the hook approved every write for as long as it existed, and every provenance quotation was unverifiable. Both are fixed; several defects are not. The file records all of them, with reproductions.


One minute

./setup.sh     # installs clingo and s(CASP) (needs Homebrew)
./demo.sh      # the whole path from code to verdict, explained step by step

demo.sh is where to start: seven steps from a source directory to a hook decision.


Where the facts come from

Three sources, and they never mix. This is the thing worth understanding.

   example-repo/                  rules/project.pl          facts/session.pl
   real source code               the team's decisions      the current session
        │                              │                         │
        │ ./extract.sh                 │ written by hand         │ human instructions,
        │ (mechanical)                 │ (layers, contexts,      │ proposed actions
        │                              │  allowlists)            │
        ▼                              │                         │
   facts/extracted.pl                  │                         │
   facts/map.txt                       │                         │
        │                              │                         │
        └──────────────┬───────────────┴─────────────────────────┘
                       ▼
              rules/codebase.pl  +  rules/general.pl     ← the rules themselves
              engine/core.pl                             ← semantics, 30 lines
                       │
          ┌────────────┴────────────┐
          ▼                         ▼
      ./check.sh                ./why.sh 'action'
      every verdict at once     circle of consideration,
      (clingo, ~3 ms)           what happened to each rule,
          │                     quotation from the source document
          ▼
   hooks/pretooluse.sh  →  Claude Code: allow / deny / ask

The key split: extract.sh decides nothing. It sees paths, packages, imports, the generated-file marker, and whether a test sits alongside. What the code cannot show — which package is domain and which is infrastructure, which boundary crossings are allowed — the team declares in rules/project.pl.


What each utility does

Command What it does What computes it
./setup.sh installs both solvers, builds the scasp command brew, swipl
./extract.sh <dir> code → facts/extracted.pl + facts/map.txt sh + extract.py
./check.sh verdicts for every action, plus notes on the corpus clingo
./why.sh '<action>' circle of consideration, per-rule outcome, quotations clingo
./why.sh '<action>' --tree plus a justification tree in words s(CASP)
./verdict.sh '<action>' one word: allowed / forbidden / undetermined / unknown clingo
./verify-sources.sh step 1 of the review: is every quotation verbatim grep
./test.sh 15 expected outcomes plus provenance clingo
./reach.sh <dir> what the corpus can say about a repository at all clingo
./demo.sh the whole path, explained all of it
hooks/pretooluse.sh asks the corpus before Write and Edit (not Bash — see D4) verdict.sh, why.sh

Invocations

./extract.sh example-repo            # or ./extract.sh ../my-service
./check.sh

./why.sh "write(internal_billing_domain_go)"      # forbidden: domain pulls in the database
./why.sh "write(internal_billing_ledger_go)"      # allowed: the pair is on the allowlist
./why.sh "write(internal_api_api_pb_go)"          # allowed: an instruction applied
./why.sh "write(config_prod_env)"                 # strictly forbidden; an instruction does not help
./why.sh "add_dep(fancylog)"                      # UNDETERMINED - a human decides
./why.sh "write(internal_billing_ship_go)" --tree # plus the s(CASP) tree

An action names an identifier from facts/map.txt (paths become atoms, because clingo does not accept quoted ones). To see the mapping:

column -t -s"$(printf '\t')" facts/map.txt

What the output shows

$ ./why.sh "write(internal_billing_domain_go)"
ACTION: write(internal_billing_domain_go)

CIRCLE OF CONSIDERATION - rules that bore on this action:
  for   r_cb_default           docs/approach.md
           «permits by default: writing code is normal, and the»
  against  r_cb_layer             example-repo/docs/architecture.md
           «Domain code must not depend on transport, database or serialization types»
  against  r_cb_boundary          example-repo/docs/architecture.md
           «organized first by bounded context rather than by technical layer»

VERDICT: FORBIDDEN

WHAT HAPPENED TO EACH RULE IN THE CIRCLE:
  r_cb_default     overruled by: r_cb_layer r_cb_boundary
  r_cb_layer       APPLIED
  r_cb_boundary    APPLIED

The circle is the point. A rule the agent never read still shows up: the engine holds every declared rule at once, whatever is in the context window.

The three outcomes map onto three hook decisions: forbidden → deny, undetermined → ask (a question for the human), otherwise silence. A fourth answer, unknown, means the action is not in the declared domain — a typo, or a file the corpus has never seen. It is deliberately not the same as undetermined: conflating them makes a typo look like a normative gap.


Layout

demo.sh  check.sh  why.sh  verdict.sh  extract.sh  verify-sources.sh  test.sh  setup.sh

engine/core.pl        defeasible-rule semantics - 30 lines, the whole "engine"
rules/codebase.pl     the codebase corpus: layers, boundaries, packages, deps
rules/general.pl      the process corpus: authority, branch, statuses
rules/project.pl      the team's decisions: layers, contexts, allowlists  ← by hand
rules/sources.pl      provenance: rule → document → verbatim quotation
rules/vocabulary.pl   closedness declarations (needed by s(CASP) only)
rules/helpers.pl      derived predicates, the domain of actions

facts/extracted.pl    ← extract.sh, do not edit
facts/map.txt         ← extract.sh: identifier → path
facts/session.pl      human instructions and proposed actions

example-repo/         real Go code that breaks exactly the right rules
hooks/                the PreToolUse hook and a settings example
docs/                 approach, review procedure, prior art

Built on

  • clingo 5.8 (Potassco) — an ASP grounder and solver. Computes the whole corpus in one pass: ~3 ms for the solver itself, ~100 ms for ./check.sh end to end on a 256-file repository. The two numbers are three orders apart and the README used to quote only the first. Its diagnostic atom does not occur in any rule head is a free check on the corpus itself;
  • s(CASP) 1.1 on SWI-Prolog 10 — a goal-directed solver without grounding, producing justification trees in words; with --unknown=error it refuses to derive anything from an undefined predicate;
  • Defeasible Logic (Nute; Governatori's line) — strict rules, defeasible rules, defeaters, a superiority relation. Translation into logic programming: arXiv:cs/0511055;
  • Claude Code hooks — the point at which consulting the corpus stops being voluntary.

There is no engine of our own, and none is planned.

Why two solvers rather than one: clingo computes everything in milliseconds but produces no proof tree. s(CASP) produces the tree and proves a conclusion in ~1 s, but does not finish proving that no conclusion exists. So the verdict is always asked of clingo, and s(CASP) is asked only about the side already known to hold — and on a slice of the corpus, which is what turns 106 s of not finishing into 0.05 s. Details in docs/approach.md.


Before you use this on a real project

Run ./reach.sh <your-repo> first. It prints which rules said anything about your code and the whole fact census, so you can see in one screen whether the corpus can express what you care about.

Then, honestly:

  • Do not gate anything that matters on this yet. The hook is not a security boundary: it fires on Write and Edit, a shell command walks past it, and it judges the file as it stands rather than the content being written;
  • The two numbers at the top of this file are the ceiling, not a starting point. If your rules are about who may merge, what needs review, or what must change together, this cannot express them — the extractor reads a working tree, and those norms are about a change and an actor;
  • What does work: rules about where code may live, which package may import which, what is generated or vendored, and who may edit the rules themselves. Those are file-shaped, and file-shaped is what the vocabulary covers;
  • ./verify-sources.sh belongs in your CI. A rule whose quotation no longer matches its document suspends the whole corpus, which is the intended behaviour and useless if nothing runs it.

Scope

A rule a transformation can enforce does not go into the corpus. "No comments in code", "hoist repeated strings into constants", "run the formatter" — that is a linter pass, cheaper than any check, and the agent need not be involved at all. The corpus is for rules where no transformation exists: rules with exceptions, rules about authority, rules that may turn out undetermined.

The corpus has not been reviewed by a human yet. It was extracted from documents and, until read through, is a retelling rather than a source. The five-step procedure is in docs/verification.md; two of the five steps are not machine-checkable and do not pretend to be.

The hook was broken until an agent tested it live. It compared the absolute path Claude Code sends against the relative paths in facts/map.txt, never matched, and approved everything. Fixed, with the normalisation spelled out in hooks/pretooluse.sh. The lesson is in the repository rather than in a claim: a hook exercised only with hand-written input is untested. To check for yourself:

./extract.sh example-repo        # a clone has no facts: they are generated
mkdir -p .claude && cp hooks/settings.example.json .claude/settings.json
claude          # then ask it to edit example-repo/internal/billing/domain.go

The first line is not optional. facts/ is generated and gitignored; without it the hook knows no files and approves everything — which is exactly how it shipped once.


How this was made

Built in Claude Code (Claude Opus 5) in a single session, together with the repository's author. Verified on macOS 15 / arm64, clingo 5.8.2, SWI-Prolog 10.0.2, s(CASP) 1.1.4.

About

Repository rules an agent cannot forget: defeasible rules over clingo and s(CASP). Every answer is three-valued and carries the full circle of rules that bore on it, so a conclusion drawn from an incomplete reading becomes detectable.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages