Executable design intent for AI-coded repos: the design document becomes the charter.
Project description
charter — the design document becomes the charter
Your design doc says:
Auth tokens are HMAC, never JWT.
$ charter annotate SPEC.md # LLM turns prose into enforceable decisions
$ cat CHARTER.md
[D-001] Auth tokens are HMAC, never JWT -> assert: ! grep -rqE "jwt|jsonwebtoken" src
$ charter approve --why "initial review"
Weeks later, an AI agent adds JWT code.
$ charter check
FAIL D-001 "Auth tokens are HMAC, never JWT" — assert FAILED
The agent fixes it. Nobody was interrupted, nothing was forgotten, and
charter trace D-001 shows every file that implements the decision.
That's the whole idea: executable design intent for AI-coded repos — ADRs + linter + traceability, in one file with almost no state.
The doctrine in one sentence: a decision with no jurisdiction is not
governed. Every decision names an enforcer; a supervise-only decision with
neither code citations nor an @ watch scope fails check. CHARTER.md is the
constitution, check is the court, audit is the judge for gray areas, and
[D-xxx] citations are the map.
Try it in 30 seconds: sh demo/run_demo.sh (on Windows, run it from Git
Bash) — an agent adds Supabase to a local-first app and check catches it.
No API key needed.
Quick start
See it work first — the demo is offline and needs no API key (on Windows, run it from Git Bash):
git clone https://github.com/cspergel/Charter
cd Charter
sh demo/run_demo.sh # an agent adds Supabase to a local-first app; check catches it
Then use it on your own repo. There are two ways to run it:
# Option A — zero install. It's one file, zero dependencies (Python 3.10+).
python /path/to/charter.py check
# Option B — install the `charter` command.
pip install charter-intent # the command is `charter`; `charter` was taken on PyPI
# — or from a local checkout: pip install .
charter check
annotate (turning a prose doc into decisions) needs an LLM backend; point it
at Claude Code with no API key required:
export CHARTER_LLM_CMD="claude -p" # or set ANTHROPIC_API_KEY
charter annotate SPEC.md
charter approve --why "initial review"
charter check
charter doctor # checks your setup is sound
State on disk: CHARTER.md (yours), .charter/ledger.jsonl (append-only
journal), and .charter/charter.sha (approval hash — commit it). Permission to
execute asserts is recorded in a per-user trust store outside the repo
(~/.charter/trust, keyed by repo path), so nothing a repo ships can grant
itself execution.
Security, in one paragraph
charter check executes shell commands defined in CHARTER.md (the assert
enforcers). A freshly cloned repo will not execute its asserts — even if it
ships an approval hash, and even if it ships a forged trust marker — until you
review CHARTER.md and run charter approve yourself, because the trust record
lives in a per-user store outside the repo. CI opts in explicitly with
CHARTER_TRUST_ASSERTS=1. annotate/audit send doc and file contents to
your configured LLM backend; nothing else makes network calls. Details and
threat model: SECURITY.md.
The lifecycle
charter annotate SPEC.md # LLM reads your prose doc, extracts binding
# decisions, assigns [D-xxx] symbols, proposes
# the lowest viable enforcer per decision,
# writes CHARTER.md + SPEC.annotated.md
# (charter init creates an empty CHARTER.md
# if you'd rather write it by hand)
<review CHARTER.md once> # the only mandatory human moment: adjust
# enforcers, strengthen supervise items
charter approve --why "..." # the human gate: journaled, hash-stamped
charter check # deterministic, free — pre-commit + CI
charter audit # judged pass over supervise-tier (PR-time)
charter digest [--mark] # batch-review everything the system did
charter trace D-001 # everything that traces to a decision
charter graph [--json] # the derived graph (Mermaid / machine-readable)
charter explain D-001 # the full story of one decision
charter doctor # setup-health checks
charter install-hook # pre-commit hook + Claude Code settings block
The line syntax
[D-001] title -> assert: <must-pass> !! <proof-must-succeed> @ glob, glob
[D-002] title -> supervise @ src/db/**
- The enforcer kind is one of the ladder below.
!!introduces an assert's tripwire: a probe that must succeed, proving the detector can detect a known violation sample (a typo'd grep path can't pass forever). The canonical pattern isecho <violation-sample> | <the-real-detector>.@declares watch scope: a human-set jurisdiction floor. Audit reads cited files ∪ watched files, so an uncited violating file is still seen.
The five layers
- Annotate — the bootstrap agent. Extraction is conservative
(contracts, not preferences — "keep the code simple" is correctly
ignored), capped at 15 by default (
--cap N), dedupes against decisions already indexed, and annotation of your original doc is non-destructive (writes a.annotatedcopy with symbols inlined at the source sentences). - Enforce — the ladder, strongest first:
structure > type > test > lint > assert > supervise. During review, push supervise items toward the stronger deterministic rungs.checkfails on: aspirational decisions (no enforcer), missing enforcer targets, enforcer rot (the#Symbolvanished in a refactor), failing asserts, and blind decisions (supervise-only with no citations and no watch scope). A proposed enforcer that doesn't exist yet is a build obligation — check stays red until the builder creates the type/test, which is governance generating the skeleton of the system.check --budget Nwarns when judgment-only decisions outgrow the budget (default 5). - Trace — builders leave
[D-xxx]citations in comments and commits. The graph is derived from grep on every run, never stored — so it can never go stale. - Supervise —
auditjudges only the supervise tier, and citations are the scope: the auditor reads exactly the files that claim to implement the decision, plus watched files. Verdicts: COMPLIES (ok line, exit 0), VIOLATES (exit 1 — fix the code), AMBIGUOUS (flagged for digest review). All verdicts land in the ledger. No backend configured → everything AMBIGUOUS, never crashes. - Steer — one optional SessionStart hook injects the whole index
(~15 one-liners, a few hundred tokens, once per session) plus the
citation protocol. No gates: agents stay in their native loop — cite the
symbol, keep
checkgreen.
LLM backends
Resolution order for annotate and audit:
CHARTER_LLM_CMD— any command that reads the prompt on stdin and prints the reply on stdout. Point it at Claude Code headless:export CHARTER_LLM_CMD="claude -p"— rides your existing plan.ANTHROPIC_API_KEY— direct API (Sonnet for annotation quality, Haiku for cheap audit verdicts; override withCHARTER_ANNOTATE_MODEL/CHARTER_AUDIT_MODEL).- Neither —
annotateexplains itself;auditdegrades to AMBIGUOUS.
Integration
.git/hooks/pre-commit (or just run charter install-hook):
python charter.py check || exit 1
CI on PRs (the judged layer) — note the explicit opt-in, and treat PRs that modify CHARTER.md like PRs that modify your CI workflows:
CHARTER_TRUST_ASSERTS=1 python charter.py check && python charter.py audit
.claude/settings.json (optional steering):
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "command": "python charter.py hook" } ] }
]
}
}
For non-hook agents (Cursor, aider), put this in AGENTS.md/.cursorrules:
This repo's binding decisions live in CHARTER.md. When your work implements or touches a decision, leave its
[D-xxx]symbol in a nearby comment and your commit message. Runpython charter.py checkbefore finishing; a failure means an enforcer caught a violation — fix the code, never the enforcer. Conflicts between a request and a decision must be surfaced, not silently resolved.
Liberties taken, and why
- Citations replace scope globs. The derived graph defines what each
decision governs. This deleted the lockfile, ack protocol, session
baselines, and per-edit hooks of earlier designs — the single largest
overhead reduction — at the cost of relying on builders to cite. The
steering hook + agent instructions make citing the path of least
resistance, and
checkfails supervise decisions that end up blind. - Stateless judgment.
auditjudges current state, not drift-since — so there is nothing to pin, ack, or reconcile. Run it whenever; the ledger is the only memory, and it's append-only and reviewable. - The graph navigates; enforcers govern.
graph --jsonexists for agents to ask "what connects to what," but no verdict ever comes from graph topology — authority lives in things that can't be argued with.
FAQ
Isn't this just a pre-commit hook that runs grep? At the deterministic layer, yes — and that's the point. A grep that can't be argued with beats an LLM that can be talked out of a verdict. Charter's value isn't a cleverer check; it's turning prose decisions into checks at all, keeping them in sync with the code via citations, and proving the checks aren't vacuous (tripwires). The grep is a feature, not an embarrassment.
Why not Spec Kit / OpenSpec / Kiro? Those govern code generation — write a spec, then generate from it. They're out of the loop the moment a later change quietly contradicts the original design, which is where drift actually accrues. Charter governs the repo from then on, on every commit. They compose: scaffold with whatever you like, keep it true with Charter.
An LLM wrote my enforcement rules — why would I trust that?
You don't trust it — you review it. annotate only proposes; nothing takes
effect until you read CHARTER.md and approve it, exactly like reading code
you're about to run. At enforcement time there's no LLM in the loop: check is
deterministic shell. The model proposes, you ratify, grep decides.
What stops an agent from editing CHARTER.md, or weakening an enforcer, to make
check pass?
Any change to CHARTER.md fails check until a human runs approve (a hashed,
journaled gate) — so an agent can't quietly rewrite the constitution. The agent
instructions say fix the code, never the enforcer, and a weakened assert trips
its tripwire (the proof that it can still catch a known violation). Tampering is
visible, not silent.
Won't grep-based asserts be brittle and false-positive?
Some will — which is why the ladder exists. Push fragile checks up to type,
test, or lint, where the language and your test runner do the work; reserve
assert for things that genuinely are a grep. Tripwires flag asserts that have
quietly stopped detecting anything, so a brittle check fails loudly rather than
passing forever.
Does it lock me into Claude?
No. Any backend that reads a prompt on stdin works (CHARTER_LLM_CMD), the
Anthropic API works, and you can skip the LLM entirely and write CHARTER.md by
hand — check never calls a model.
Known limits
checkexecutes shell from CHARTER.md. The trust gate means a cloned repo can't run code on your machine before you review it, but after you approve, the asserts are exactly as trustworthy as your review of them. Read them like code you are about to run — because they are.- A builder that never cites makes citation-only supervise decisions blind —
check fails them rather than letting them silently un-govern. Decisions
with
@watch globs are still audited via watched files. Deterministic rungs are immune — prefer them. - Annotation quality is bounded by the doc: vague prose yields supervise proposals. The review-once step is where you strengthen enforcers.
assertcommands are POSIX-shell; on Windows they run under Git Bash (auto-detected,CHARTER_SHELLoverrides).- A vacuous assert's
!! proofis authored by the same source as the assert, so the tripwire raises the bar but can't fully self-police —checkflags trivially-true probes (!! true, bareecho), but a cleverly matched fake proof still needs the human review-once gate. auditjudges at most the first 60 in-scope files per decision (chunked, worst-verdict-wins) to bound LLM cost; broad@ src/**scopes over huge trees are reported as truncated. Narrow watch globs audit completely.auditsends in-scope file contents to the model, and a determined prompt-injection in governed code can still influence a verdict — deterministic rungs carry the real authority;supervise+auditis the soft, advisory tier.
History
See CHANGELOG.md. Charter was previously named governor; v0.4.0 renamed it and added the local trust gate.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file charter_intent-0.4.2.tar.gz.
File metadata
- Download URL: charter_intent-0.4.2.tar.gz
- Upload date:
- Size: 39.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe08633e26b15c78d8ae10ea7929d16987934da0a0460ed2368548471bd118b6
|
|
| MD5 |
d18311e95348342b2a552b6cefd58827
|
|
| BLAKE2b-256 |
401b0c2c859a54b7af59a0a82329ece132224715c16ca2a6e21ffa79684bb199
|
Provenance
The following attestation bundles were made for charter_intent-0.4.2.tar.gz:
Publisher:
workflow.yml on cspergel/Charter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
charter_intent-0.4.2.tar.gz -
Subject digest:
fe08633e26b15c78d8ae10ea7929d16987934da0a0460ed2368548471bd118b6 - Sigstore transparency entry: 1819150012
- Sigstore integration time:
-
Permalink:
cspergel/Charter@0790d9e50cb581f97346cd2974d0798d086e8318 -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/cspergel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@0790d9e50cb581f97346cd2974d0798d086e8318 -
Trigger Event:
release
-
Statement type:
File details
Details for the file charter_intent-0.4.2-py3-none-any.whl.
File metadata
- Download URL: charter_intent-0.4.2-py3-none-any.whl
- Upload date:
- Size: 26.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee4a4b5b9e77dcd4660f1bc2f76ef85cfc8f21db0b24507f8b7d274c083f72b7
|
|
| MD5 |
6b318807c7097d977ec39170db4f1d7a
|
|
| BLAKE2b-256 |
1cc7f88422efdba3b6860c5090d8e4f309523cca6f1451d2588e86a640aa7161
|
Provenance
The following attestation bundles were made for charter_intent-0.4.2-py3-none-any.whl:
Publisher:
workflow.yml on cspergel/Charter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
charter_intent-0.4.2-py3-none-any.whl -
Subject digest:
ee4a4b5b9e77dcd4660f1bc2f76ef85cfc8f21db0b24507f8b7d274c083f72b7 - Sigstore transparency entry: 1819150414
- Sigstore integration time:
-
Permalink:
cspergel/Charter@0790d9e50cb581f97346cd2974d0798d086e8318 -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/cspergel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@0790d9e50cb581f97346cd2974d0798d086e8318 -
Trigger Event:
release
-
Statement type: