Grace17 for AI agents
This guide is for people building agents that prove theorems in Grace17, and for the agents themselves. It describes the interfaces an agent can call, what each returns, and a working loop that uses them well. If your agent changes the Grace17 code base itself, read AGENTS.md instead.
The contract is the same for an agent as for a person. A proof is admitted only if the kernel checks it. Tactics, search, and suggestions are untrusted conveniences, so nothing an agent produces can admit a false statement. What an agent can do is explore quickly, read goal states, search the library, and hand a human a checked result with its assumptions listed.
Working with Claude Code#
Grace17 ships a Claude Code skill: the write–check loop, the module header, the proof style Cumulus expects, how theorem application infers its parameters, and the elaborator pitfalls that cost the most time. There are three ways to have it.
Inside this repository nothing needs installing. The skill is at
.claude/skills/grace17-proving/, and Claude Code run in a clone of the repository loads it when a task calls for it;/grace17-provinginvokes it by hand.In your own projects, install it as a plugin from inside Claude Code:
/plugin install grace17 --marketplace grace17-prover/grace17Older versions of Claude Code take it in two steps:
/plugin marketplace add grace17-prover/grace17, then/plugin install grace17@grace17. The repository is its own marketplace (.claude-plugin/marketplace.json), and the plugin lives inplugins/grace17. Plugin skills are namespaced, so the skill answers to/grace17:grace17-proving./plugin marketplace update grace17picks up new versions.By hand, copy
plugins/grace17/skills/grace17-proving/into~/.claude/skills/to make it available everywhere without the plugin.
python3 scripts/all-errors.py <file> lists the first error of every theorem
in a module in one run, so an agent can plan its repairs instead of
discovering them one check at a time.
The machine interface#
Everything the Workbench shows is available as structured data with the
same semantics through the machine interface:
g17 serve speaks newline-delimited JSON over stdio, and each method is
also a subcommand. The methods are check (per-theorem status, the goal
state after every step, diagnostics with positions, the partial trace on
failure, the axiom audit on request), goal (the state at a position),
suggest (exact?, apply?, or rw? at a position, each suggestion as
the command that pastes plus its theorem, instantiation, and evidence),
search, declaration, audit, library, and notations. An agent
that loops over check, goal, suggest, and paste should use it rather than
read printed text.
node bin/g17.ts check --json --module-root lib Probe.g17
node bin/g17.ts goal Probe.g17 --line 11 --column 12
node bin/g17.ts suggest Probe.g17 --line 12 --tactic 'exact?'
node bin/g17.ts search 'name:transitive module:Set.Core'Interfaces at a glance#
| Interface | Use it for | Returns |
|---|---|---|
g17 serve, g17 check --json, g17 goal, g17 suggest |
A tool or agent driving the checker | The Workbench's states, suggestions, search results, declarations, and audits as JSON; see machine interface |
g17 check --artifacts build/modules |
Checking files from a shell | Pass or fail; on failure the message, line, and the remaining goals with named hypotheses (--json adds the full tactic trace and every theorem's step states); with --artifacts, library imports replay from compiled artifacts in about a second |
Workbench HTTP API, POST /api/check |
Structured proof states | Every tactic's before and after goals, named hypotheses, search suggestions, sorry goals, statements, dependencies |
TypeScript API, checkWorkbenchFiles |
The same data in-process | The same structure, without a server |
g17 axiom-audit --json |
What a proof depends on | Named axioms, schema instances, Choice usage, dependency paths |
| Library search | Finding existing results | Qualified names, statements, source locations |
g17 export, independent checkers |
Handing off a certificate | A proof tape replayable by the Python, OCaml, and Rust checkers |
The structured checking API#
Start the Workbench server once; it also builds the browser bundle:
npm run workbenchThen post a workspace. Each file is a name and its full source. activeFile
selects which file's theorems are reported in detail; the others are context.
curl -s http://127.0.0.1:1717/api/check \
-H 'content-type: application/json' \
-d '{"files":[{"name":"Probe.g17","source":"module Probe\nimport Grace.Cumulus.Set.Core\n\ntheorem subset_chain (A B C : Set)\n requires first : A ⊆ B\n requires second : B ⊆ C\n shows A ⊆ C\nproof\n unfold Subset\n intro x\n intro hx\n"}],"activeFile":"Probe.g17"}'The proof above stops one step short. The response says so and includes the whole trace. Abridged to its last step:
{
"ok": false,
"kind": "surface",
"message": "1 goal remains; next is x ∈ C",
"line": 11,
"trace": {
"steps": [
{
"line": 11,
"command": "intro hx",
"after": {
"goals": [
{
"goal": "x ∈ C",
"assumptions": ["A ⊆ B", "B ⊆ C", "x ∈ A"],
"locals": [
{ "name": "first", "formula": "A ⊆ B" },
{ "name": "second", "formula": "B ⊆ C" },
{ "name": "hx", "formula": "x ∈ A" }
]
}
]
}
}
]
}
}Three fields matter most. goal is the formula to prove. locals are the
hypotheses by name, which is what you cite in exact ... with first, hx.
assumptions are the same formulas without names, in context order.
When a file checks, the response has ok: true and a theorems array. Each
theorem carries its statement, its steps with the same before and after
states, its sorries, and its dependencies (axioms, theorems, definitions).
The same function is available in-process, which is faster for many calls:
import { checkWorkbenchFiles, workbenchFailure } from "./src/index.ts";
let result;
try {
result = checkWorkbenchFiles(files, "ZFG", ["Probe.g17"], { includeKernelDetails: false });
} catch (error) {
result = workbenchFailure(error); // same shape as the HTTP failure response
}The HTTP server fixes the foundation to ZFG. The in-process call and the
command line accept ZF, ZFC, or ZFG.
Use sorry as a probe#
A proof that ends in sorry checks successfully, so you get the complete
step trace and a sorries entry recording what remains:
"sorries": [
{ "theorem": "Probe.subset_chain", "line": 12, "goal": "x ∈ C",
"axiom": "sorry.Probe.subset_chain.12.1" }
]This is the most useful pattern for an agent: write the steps you are sure
of, end with sorry, read the remaining goal with its hypotheses, and
continue. A sorry introduces a named axiom, so it also appears in the axiom
audit and in dependencies.axioms. Cumulus accepts nothing that contains a
sorry.
Query tactics return commands you can paste#
exact?, apply?, and rw? run bounded, deterministic search over the
imported environment. Their results appear in the step's info field as
fully qualified commands:
{
"command": "apply?",
"info": {
"title": "apply?",
"items": [
"apply Grace.Cumulus.Set.Core.powerset.membership_iff.forward at A, C",
"apply Grace.Cumulus.Set.Basic.Set.Subset.equation.backward at A, C"
]
}
}Replace the query tactic with the suggested line. A finished proof should
not contain ? tactics. The written form is faster to replay, stable when
the library grows, and readable by the person who reviews it. When search
finds nothing, the message says so; search the library yourself or prove the
missing lemma.
Finding theorems#
Three complementary routes:
- Library search.
g17 search 'name:mul module:Complex'runs the Workbench's search from the shell, and thesearchmethod of the machine interface runs it over the protocol. It matches words in names, statements, and section context, withname:,module:,kind:,symbol:,conclusion:, andpattern:criteria; see library search for the syntax. In-process, the index issrc/library-search.ts: constructnew LibrarySearchWorkspace(files)over a map of file path to source and callworkspace.index.search(query);scripts/benchmark-library-search.tsis a complete example. - Grep the source. Cumulus is plain text under
lib/Grace/Cumulus/. Names are dotted and descriptive:Subset.transitive,Zorn.theorem,FTC.derivative_of_integral. Searching for the statement's key symbols or for the subject's namespace usually lands in the right module. - The guide. cumulus.md lists what each module provides and which entry point to import for a subject.
Prefer applying an existing theorem to re-proving it. If a lemma is missing, add it to the module where it belongs. Do not inline it in your proof.
A working loop#
- State the theorem first. Write
requireshypotheses with names and ashowsclause. Check the file with the proof bodysorry. If the statement does not elaborate, the message names the line and the problem, usually a missing import or an unresolved name. - Read the goal. From the
sorriesentry or the trace, take the goal and the named locals. - Try automation on the goal.
ringfor polynomial identities,decidefor closed statements about finite sets (membership, inclusion, equality, cardinality over set literals),linarithfor linear inequalities over the reals and rationals (products are atoms, so state nonlinear facts such as0 ≤ x * yas hypotheses first),arithfor linear arithmetic over naturals,simp [rules]for rewriting, definitional and logical closing, and membership in compound sets (the membership lemmas andSubsetare automatic),typecheckfor carrier obligations. See tactics. A tactic that fails costs nothing but time. - Decompose.
unfolda definition,introbinders and premises,obtain ⟨x, hx⟩ := hfor existentials,casesfor disjunctions,have h : Pfor an intermediate fact. Re-check after each step and read the new goal. - Apply library results. Use
apply?or search to find a candidate, then write it out:exact Subset.transitive with first, second, orhave h := theorem at args with premises. - Finish and audit. Remove every
sorryand?. Run the axiom audit and check that Choice appears only if it is needed. Check underZFif the subject permits.
Checking the probe file above from the command line takes about half a second. Files that import large subjects such as real analysis take longer, because their imports are elaborated too; the in-process API keeps a loaded workspace between calls.
Foundations and the axiom audit#
The command line accepts --foundation ZF, ZFC, or ZFG. A theorem checked
under ZFG may still have a Choice-free proof; the audit tells you:
node bin/g17.ts axiom-audit --json --foundation ZF --module-root lib Probe.g17 --theorem Probe.subset_chainThe report lists direct and hereditary named axioms, schema instances (Separation, Replacement, datatypes, recursion), classical double-negation use, and a dependency path to each axiom. An incomplete audit exits with status 1 and cannot certify the absence of Choice. See axiom audits.
Handing off a result#
Before presenting a theorem as done:
- the file checks with no
sorryand no query tactics; - the axiom audit is complete and its Choice usage is stated;
- you have read the statement as mathematics. The kernel guarantees the proof; whether the statement says what you meant is your call;
- optionally, the certificate has been exported and replayed by an independent checker:
node bin/g17.ts compile --module-root lib Probe.g17 --out probe.g17m --environment probe.g17env
python3 checkers/python/g17check.py probe.g17m --environment probe.g17envReport which of these you ran. A human takes ownership of anything submitted to Cumulus; see CONTRIBUTING.md.
Known limitations#
These are the gaps most likely to matter to an agent builder today:
- The HTTP server checks under ZFG only;
g17 serveand the subcommands accept afoundation. - Query-tactic search is bounded. In the example above,
exact?did not findSubset.transitivefor the goalA ⊆ CfromA ⊆ BandB ⊆ C, whileexact Subset.transitive with *closes it in one line. When search fails, name the theorem yourself. linarithinterprets only addition, subtraction, and negation over the reals and rationals; products are atoms. Nonlinear facts such as0 ≤ x * ymust be stated as hypotheses (for example fromrealMulValue.nonnegative) before the linear step.
Pitfalls that cost an agent iterations#
The checker's message is quoted for each so it can be matched.
apply thm at argsleaves carrier guards first. The goals afterapplyareA ∈ Real,B ∈ Real, then the premise, so a followingintrofails with "intro requires a universal or implication goal". Preferhave h : P by tacticfor the premise, thenexact thm with h; theatlist is only needed when the goal and the named premises leave a parameter undetermined.- A negated lemma does not apply itself.
exact realLe.not_less at A, B with *establishes¬(B < A)but the goal is⊥: list the contradicting fact after the premises,with *{2}, le, lt. The message is "proof establishes ¬..., but the goal is ⊥". - A contradiction branch starts with
exfalso. "proof establishes ⊥, but the goal is ∃ ..." means the goal was never changed to⊥. - Numerals in set comprehensions default to
Nat. Write{ T ∈ ℝ | (0:R) ≤ (T:R) ∧ (T:R) * (T:R) ≤ X }; the message is "cannot resolve notation ≤ for operand types Nat, ?". In a real statement,-1has to be written-(1:R)for the same reason. unfoldalso rewrites with local equalities. Afterunfold f, a followingrw hcan fail with "rewrite rule does not match" because the goal is already in the rewritten form; close withccinstead.- Two soft-typed operands may not resolve notation. For
(X Y : NonnegativeReal),X ≤ Yfails with "cannot resolve notation ≤ for operand types NonnegativeReal|Real, NonnegativeReal", and(X:R)is rejected becauseX"already has type NonnegativeReal". Take(X Y : Real)andrequires nonnegativeX : NonnegativeReal(X). defuniquebinders areSet,Nat, orRealonly. Hypotheses go inrequires; adefthat wraps the value with a declared result type then needs an attachedproofblock for its.typeobligation.- Generated names can collide.
defunique fpublishesf.exists,f.unique, andf.selected; a theorem namedf.existsnearby fails with "Duplicate proposition name" reported at thedefuniqueline. with *only looks at local facts. It will not fetch a library theorem or provet = tfor you. Bindhave one := realZero.lt_oneorhave same : T * T = T * Twithrflfirst, or name the premise.- Destructure predicates directly. The style gate rejects
unfold P at hfollowed byrcases h with ...("recursive destructuring follows transparent predicates directly"); writercases h with ...alone. with *already fills declaredrequirespremises. Listing one again,lemma at a, b with *, different, fails with "has no remaining argument at different". Name a premise after the star only for an implication inside the conclusion, as inwith *, positiveforshows 0 < n → ….?_takes the whole remaining proposition. Inrefine ⟨w, h, ?_, ?_⟩against∃ z, A ∧ (B ∧ C), the first?_capturesB ∧ Cand the message is "refine has 1 unused constructor entry". Use_for leaf holes when several goals are wanted.rwdoes not rewrite under a binder. Withequal : y = xand a goal∀ q, …or∃ zs, …, the message is "rewrite rule does not match";introsorrefinefirst, then rewrite inside the component.- Nat
arithneeds a comparison goal. Afterexfalsoit reports "arith requires a linear equality or comparison goal". Derivehave bad : p < pbyarithand finish withLess.irreflexive at p with *, bad. - Nat
arithproves comparisons more readily than equalities. When "arith found no supported linear arithmetic step" appears on an equality goal, provex ≤ 0(or both directions) byarithand finish withLessEqual.antisymmetric. - Nat
ringcarries numeral coefficients.2 * (q * q) = q * q + q * qand(4 * a + 2) * (a + 1) = 4 * a * a + 6 * a + 2are ring identities; a normal form writes each monomial ascoefficient * product, so aring_nfresult reads4 * (x * y)rather than four copies. - Successor injectivity is a generated law. Nat
arithdoes not derivek = cfromsucc(k) = succ(c); useNaturals.injective.succ at k, c with h. - A bounded existential takes only the first disjunct.
∃ S ∈ X, P ∨ Qparses as(∃ S ∈ X, P) ∨ Q; write∃ S ∈ X, (P ∨ Q). - A natural embedded in ℝ needs its carrier in statements.
realNat(k) + 1in a theorem statement fails with "cannot resolve notation + for operand types Set, Nat"; write(realNat(k):R) + 1. Afterrw realNat.succthe goal showsrealAddValue(…), whichlinarithandringread butrwwith a+fact does not; restate the law in operator form once.