alephprover 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alephprover-0.0.1/.github/workflows/publish.yml +48 -0
- alephprover-0.0.1/.gitignore +9 -0
- alephprover-0.0.1/LICENSE +21 -0
- alephprover-0.0.1/PKG-INFO +95 -0
- alephprover-0.0.1/README.md +71 -0
- alephprover-0.0.1/pyproject.toml +35 -0
- alephprover-0.0.1/src/alephprover/__init__.py +3 -0
- alephprover-0.0.1/src/alephprover/cli.py +392 -0
- alephprover-0.0.1/src/alephprover/prove.md +41 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
dry_run:
|
|
10
|
+
description: "Dry run (build only, do not upload)"
|
|
11
|
+
type: boolean
|
|
12
|
+
default: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
runs-on: ubuntu-24.04
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
|
|
24
|
+
- name: Install build tools
|
|
25
|
+
run: pip install build
|
|
26
|
+
|
|
27
|
+
- name: Build package
|
|
28
|
+
run: python -m build
|
|
29
|
+
|
|
30
|
+
- uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-24.04
|
|
38
|
+
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && !inputs.dry_run)
|
|
39
|
+
environment: pypi
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/download-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
|
|
48
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Logical Intelligence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alephprover
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: CLI tool for submitting Lean proof requests to the Aleph Prover API
|
|
5
|
+
Project-URL: Homepage, https://alephprover.logicalintelligence.com
|
|
6
|
+
Project-URL: Repository, https://github.com/logiq-ai/alephprover
|
|
7
|
+
Author: Logical Intelligence
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: formal-verification,lean,lean4,proof-assistant,theorem-proving
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: click>=8.0
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Aleph Prover CLI
|
|
26
|
+
|
|
27
|
+
Prove Lean4 theorems with the [Aleph Prover](https://alephprover.logicalintelligence.com) API from your terminal or from [Claude Code](https://claude.ai/code).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Run directly with uvx (no install needed)
|
|
33
|
+
uvx alephprover prove <file_path> <theorem_name>
|
|
34
|
+
|
|
35
|
+
# Or install globally
|
|
36
|
+
uv tool install alephprover
|
|
37
|
+
|
|
38
|
+
# Or with pip
|
|
39
|
+
pip install alephprover
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Set your API key (get one at https://alephprover.logicalintelligence.com/account)
|
|
46
|
+
export PROVER_API_KEY="sk-aleph-..."
|
|
47
|
+
|
|
48
|
+
# Submit a proof request
|
|
49
|
+
alephprover prove Mathlib/Algebra/Group/Basic.lean mul_left_cancel
|
|
50
|
+
|
|
51
|
+
# With hints and budgets
|
|
52
|
+
alephprover prove MyProject/Basic.lean my_theorem \
|
|
53
|
+
--hints "try induction on n" \
|
|
54
|
+
--time-budget 30 \
|
|
55
|
+
--cost-budget 10
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The CLI will:
|
|
59
|
+
1. Find the Lean project root (walks up to `lakefile.lean` / `lakefile.toml`)
|
|
60
|
+
2. Zip the project (excluding build artifacts)
|
|
61
|
+
3. Upload to the API and poll for completion
|
|
62
|
+
4. Download and apply the proof diff via `git apply`
|
|
63
|
+
|
|
64
|
+
## Claude Code Skill
|
|
65
|
+
|
|
66
|
+
Install the `/prove` skill for [Claude Code](https://claude.ai/code):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Install in current project
|
|
70
|
+
uvx alephprover install-skill
|
|
71
|
+
|
|
72
|
+
# Or install globally (available in all projects)
|
|
73
|
+
uvx alephprover install-skill --global
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then in Claude Code:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
/prove mul_left_cancel in Mathlib/Algebra/Group/Basic.lean
|
|
80
|
+
/prove my_theorem in MyProject/Basic.lean with hint: try induction on n
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
| Variable | Required | Default | Description |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| `PROVER_API_KEY` | Yes | — | API key (`sk-aleph-...`) from [Account Settings](https://alephprover.logicalintelligence.com/account) |
|
|
88
|
+
| `PROVER_API_URL` | No | `https://alephprover.logicalintelligence.com` | API base URL |
|
|
89
|
+
|
|
90
|
+
## Requirements
|
|
91
|
+
|
|
92
|
+
- Python >= 3.10
|
|
93
|
+
- [`uv`](https://docs.astral.sh/uv/) (for `uvx` usage) or `pip install alephprover`
|
|
94
|
+
- `git` (for applying patches)
|
|
95
|
+
- An Aleph Prover account with an API key
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Aleph Prover CLI
|
|
2
|
+
|
|
3
|
+
Prove Lean4 theorems with the [Aleph Prover](https://alephprover.logicalintelligence.com) API from your terminal or from [Claude Code](https://claude.ai/code).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run directly with uvx (no install needed)
|
|
9
|
+
uvx alephprover prove <file_path> <theorem_name>
|
|
10
|
+
|
|
11
|
+
# Or install globally
|
|
12
|
+
uv tool install alephprover
|
|
13
|
+
|
|
14
|
+
# Or with pip
|
|
15
|
+
pip install alephprover
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Set your API key (get one at https://alephprover.logicalintelligence.com/account)
|
|
22
|
+
export PROVER_API_KEY="sk-aleph-..."
|
|
23
|
+
|
|
24
|
+
# Submit a proof request
|
|
25
|
+
alephprover prove Mathlib/Algebra/Group/Basic.lean mul_left_cancel
|
|
26
|
+
|
|
27
|
+
# With hints and budgets
|
|
28
|
+
alephprover prove MyProject/Basic.lean my_theorem \
|
|
29
|
+
--hints "try induction on n" \
|
|
30
|
+
--time-budget 30 \
|
|
31
|
+
--cost-budget 10
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The CLI will:
|
|
35
|
+
1. Find the Lean project root (walks up to `lakefile.lean` / `lakefile.toml`)
|
|
36
|
+
2. Zip the project (excluding build artifacts)
|
|
37
|
+
3. Upload to the API and poll for completion
|
|
38
|
+
4. Download and apply the proof diff via `git apply`
|
|
39
|
+
|
|
40
|
+
## Claude Code Skill
|
|
41
|
+
|
|
42
|
+
Install the `/prove` skill for [Claude Code](https://claude.ai/code):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Install in current project
|
|
46
|
+
uvx alephprover install-skill
|
|
47
|
+
|
|
48
|
+
# Or install globally (available in all projects)
|
|
49
|
+
uvx alephprover install-skill --global
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then in Claude Code:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
/prove mul_left_cancel in Mathlib/Algebra/Group/Basic.lean
|
|
56
|
+
/prove my_theorem in MyProject/Basic.lean with hint: try induction on n
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
| Variable | Required | Default | Description |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `PROVER_API_KEY` | Yes | — | API key (`sk-aleph-...`) from [Account Settings](https://alephprover.logicalintelligence.com/account) |
|
|
64
|
+
| `PROVER_API_URL` | No | `https://alephprover.logicalintelligence.com` | API base URL |
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- Python >= 3.10
|
|
69
|
+
- [`uv`](https://docs.astral.sh/uv/) (for `uvx` usage) or `pip install alephprover`
|
|
70
|
+
- `git` (for applying patches)
|
|
71
|
+
- An Aleph Prover account with an API key
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "alephprover"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "CLI tool for submitting Lean proof requests to the Aleph Prover API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "Logical Intelligence"}]
|
|
13
|
+
keywords = ["lean", "lean4", "theorem-proving", "formal-verification", "proof-assistant"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"httpx>=0.27",
|
|
27
|
+
"click>=8.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://alephprover.logicalintelligence.com"
|
|
32
|
+
Repository = "https://github.com/logiq-ai/alephprover"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
alephprover = "alephprover.cli:main"
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""Aleph Prover CLI — submit Lean proof requests and apply results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
DEFAULT_API_URL = "https://alephprover.logicalintelligence.com"
|
|
18
|
+
POLL_INTERVAL = 15 # seconds
|
|
19
|
+
POLL_TIMEOUT = 60 * 60 # 60 minutes
|
|
20
|
+
|
|
21
|
+
# Root config files to always include in the archive
|
|
22
|
+
ROOT_FILES = ("lakefile.lean", "lakefile.toml", "lean-toolchain", "lake-manifest.json")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_project_root(start: Path) -> Path:
|
|
26
|
+
"""Walk up from start to find directory containing lakefile.lean or lakefile.toml."""
|
|
27
|
+
current = start.resolve()
|
|
28
|
+
if current.is_file():
|
|
29
|
+
current = current.parent
|
|
30
|
+
while current != current.parent:
|
|
31
|
+
if (current / "lakefile.lean").exists() or (current / "lakefile.toml").exists():
|
|
32
|
+
return current
|
|
33
|
+
current = current.parent
|
|
34
|
+
click.echo(
|
|
35
|
+
"Error: Could not find a Lean project root (lakefile.lean or lakefile.toml).\n"
|
|
36
|
+
"Make sure you are inside a Lean project directory.",
|
|
37
|
+
err=True,
|
|
38
|
+
)
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_api_key() -> str:
|
|
43
|
+
"""Check PROVER_API_KEY env var."""
|
|
44
|
+
key = os.environ.get("PROVER_API_KEY", "")
|
|
45
|
+
if not key:
|
|
46
|
+
click.echo(
|
|
47
|
+
"Error: PROVER_API_KEY is not set.\n"
|
|
48
|
+
"\n"
|
|
49
|
+
"To set up:\n"
|
|
50
|
+
" 1. Get an API key from https://alephprover.logicalintelligence.com/account\n"
|
|
51
|
+
' 2. export PROVER_API_KEY="sk-aleph-..."',
|
|
52
|
+
err=True,
|
|
53
|
+
)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
if not key.startswith("sk-aleph-"):
|
|
56
|
+
click.echo(
|
|
57
|
+
'Error: PROVER_API_KEY must start with "sk-aleph-".\n'
|
|
58
|
+
"Check your API key at https://alephprover.logicalintelligence.com/account",
|
|
59
|
+
err=True,
|
|
60
|
+
)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
return key
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_api_url() -> str:
|
|
66
|
+
return os.environ.get("PROVER_API_URL", DEFAULT_API_URL).rstrip("/")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_lean_src_paths(project_root: Path) -> list[str] | None:
|
|
70
|
+
"""Query Lake for source directories. Returns None if Lake is unavailable."""
|
|
71
|
+
try:
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
["lake", "env", "printenv", "LEAN_SRC_PATH"],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
cwd=project_root,
|
|
77
|
+
timeout=30,
|
|
78
|
+
)
|
|
79
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
80
|
+
return [p for p in result.stdout.strip().split(":") if p]
|
|
81
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
82
|
+
pass
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def zip_project(project_root: Path, verbose: bool = False, all_files: bool = False) -> Path:
|
|
87
|
+
"""Create a ZIP archive with only the files needed for building.
|
|
88
|
+
|
|
89
|
+
Uses Lake to discover source directories when available. Falls back to
|
|
90
|
+
including all .lean files outside .lake/.
|
|
91
|
+
"""
|
|
92
|
+
# Collect files to archive
|
|
93
|
+
files: list[Path] = []
|
|
94
|
+
|
|
95
|
+
if all_files:
|
|
96
|
+
click.echo("Warning: --all-files includes ALL project files. Archive may be large and may contain sensitive data.", err=True)
|
|
97
|
+
for p in sorted(project_root.rglob("*")):
|
|
98
|
+
if p.is_file():
|
|
99
|
+
files.append(p)
|
|
100
|
+
else:
|
|
101
|
+
src_paths = get_lean_src_paths(project_root)
|
|
102
|
+
|
|
103
|
+
# Always include root config files
|
|
104
|
+
for name in ROOT_FILES:
|
|
105
|
+
p = project_root / name
|
|
106
|
+
if p.exists():
|
|
107
|
+
files.append(p)
|
|
108
|
+
|
|
109
|
+
if src_paths is not None:
|
|
110
|
+
click.echo(f"Lake source paths: {', '.join(src_paths) or '.'}")
|
|
111
|
+
if src_paths:
|
|
112
|
+
for src in src_paths:
|
|
113
|
+
src_dir = project_root / src
|
|
114
|
+
if src_dir.is_dir():
|
|
115
|
+
files.extend(sorted(src_dir.rglob("*.lean")))
|
|
116
|
+
else:
|
|
117
|
+
# Empty LEAN_SRC_PATH means project root is the source dir
|
|
118
|
+
for p in sorted(project_root.rglob("*.lean")):
|
|
119
|
+
rel = p.relative_to(project_root).as_posix()
|
|
120
|
+
if not rel.startswith(".lake/") and not rel.startswith("lake-packages/"):
|
|
121
|
+
files.append(p)
|
|
122
|
+
else:
|
|
123
|
+
click.echo("Lake not available, including all .lean files")
|
|
124
|
+
for p in sorted(project_root.rglob("*.lean")):
|
|
125
|
+
rel = p.relative_to(project_root).as_posix()
|
|
126
|
+
if not rel.startswith(".lake/") and not rel.startswith("lake-packages/"):
|
|
127
|
+
files.append(p)
|
|
128
|
+
|
|
129
|
+
# Deduplicate while preserving order
|
|
130
|
+
seen: set[Path] = set()
|
|
131
|
+
unique_files: list[Path] = []
|
|
132
|
+
for f in files:
|
|
133
|
+
if f not in seen:
|
|
134
|
+
seen.add(f)
|
|
135
|
+
unique_files.append(f)
|
|
136
|
+
|
|
137
|
+
# Create archive
|
|
138
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".zip", prefix="alephprover-", delete=False)
|
|
139
|
+
tmp.close()
|
|
140
|
+
zip_path = Path(tmp.name)
|
|
141
|
+
|
|
142
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
143
|
+
for path in unique_files:
|
|
144
|
+
rel = path.relative_to(project_root).as_posix()
|
|
145
|
+
if verbose:
|
|
146
|
+
click.echo(f" {rel}")
|
|
147
|
+
zf.write(path, rel)
|
|
148
|
+
|
|
149
|
+
size_mb = zip_path.stat().st_size / (1024 * 1024)
|
|
150
|
+
click.echo(f"Archived {len(unique_files)} files ({size_mb:.1f} MB)")
|
|
151
|
+
return zip_path
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def submit(
|
|
155
|
+
api_url: str,
|
|
156
|
+
api_key: str,
|
|
157
|
+
zip_path: Path,
|
|
158
|
+
file_path: str,
|
|
159
|
+
theorem_name: str,
|
|
160
|
+
hints: str | None,
|
|
161
|
+
time_budget: int | None,
|
|
162
|
+
cost_budget: float | None,
|
|
163
|
+
) -> str:
|
|
164
|
+
"""Submit the archive and return the request ID."""
|
|
165
|
+
data: dict[str, str] = {
|
|
166
|
+
"file_path": file_path,
|
|
167
|
+
"theorem_name": theorem_name,
|
|
168
|
+
}
|
|
169
|
+
if hints:
|
|
170
|
+
data["hints"] = hints
|
|
171
|
+
if time_budget is not None:
|
|
172
|
+
data["time_budget_minutes"] = str(time_budget)
|
|
173
|
+
if cost_budget is not None:
|
|
174
|
+
data["cost_budget_usd"] = str(cost_budget)
|
|
175
|
+
|
|
176
|
+
with open(zip_path, "rb") as f:
|
|
177
|
+
files = {"archive": ("project.zip", f, "application/zip")}
|
|
178
|
+
response = httpx.post(
|
|
179
|
+
f"{api_url}/api/v1/requests/upload",
|
|
180
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
181
|
+
data=data,
|
|
182
|
+
files=files,
|
|
183
|
+
timeout=120.0,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if response.status_code != 201:
|
|
187
|
+
click.echo(f"Error: Upload failed (HTTP {response.status_code})", err=True)
|
|
188
|
+
try:
|
|
189
|
+
click.echo(response.json(), err=True)
|
|
190
|
+
except Exception:
|
|
191
|
+
click.echo(response.text, err=True)
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
request_id = response.json()["id"]
|
|
195
|
+
return request_id
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def poll_status(api_url: str, api_key: str, request_id: str) -> str:
|
|
199
|
+
"""Poll until terminal status. Returns final status."""
|
|
200
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
201
|
+
start = time.time()
|
|
202
|
+
last_stage = None
|
|
203
|
+
|
|
204
|
+
while time.time() - start < POLL_TIMEOUT:
|
|
205
|
+
try:
|
|
206
|
+
response = httpx.get(
|
|
207
|
+
f"{api_url}/api/v1/requests/{request_id}",
|
|
208
|
+
headers=headers,
|
|
209
|
+
timeout=30.0,
|
|
210
|
+
)
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
except httpx.HTTPError as e:
|
|
213
|
+
click.echo(f"Warning: Poll request failed ({e}), retrying...", err=True)
|
|
214
|
+
time.sleep(POLL_INTERVAL)
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
data = response.json()
|
|
218
|
+
status = data["status"]
|
|
219
|
+
stage = data.get("stage")
|
|
220
|
+
|
|
221
|
+
if stage and stage != last_stage:
|
|
222
|
+
click.echo(f" Stage: {stage}")
|
|
223
|
+
last_stage = stage
|
|
224
|
+
|
|
225
|
+
if status in ("completed", "failed", "cancelled"):
|
|
226
|
+
return status
|
|
227
|
+
|
|
228
|
+
time.sleep(POLL_INTERVAL)
|
|
229
|
+
|
|
230
|
+
click.echo(
|
|
231
|
+
f"Timeout: polling exceeded {POLL_TIMEOUT // 60} minutes.\n"
|
|
232
|
+
f"Check status at: {api_url}/requests/{request_id}",
|
|
233
|
+
err=True,
|
|
234
|
+
)
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def download_diff(api_url: str, api_key: str, request_id: str) -> Path | None:
|
|
239
|
+
"""Download the diff patch. Returns path or None on failure."""
|
|
240
|
+
response = httpx.get(
|
|
241
|
+
f"{api_url}/api/v1/requests/{request_id}/diff",
|
|
242
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
243
|
+
timeout=60.0,
|
|
244
|
+
)
|
|
245
|
+
if response.status_code != 200:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
patch_path = Path(tempfile.mktemp(suffix=".patch", prefix=f"proof-{request_id[:8]}-"))
|
|
249
|
+
patch_path.write_bytes(response.content)
|
|
250
|
+
return patch_path
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def apply_patch(patch_path: Path, project_root: Path) -> bool:
|
|
254
|
+
"""Apply patch with git apply. Returns True on success."""
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
["git", "apply", str(patch_path)],
|
|
257
|
+
capture_output=True,
|
|
258
|
+
text=True,
|
|
259
|
+
cwd=project_root,
|
|
260
|
+
)
|
|
261
|
+
if result.returncode == 0:
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
# Try --3way fallback
|
|
265
|
+
click.echo("Standard apply failed, trying --3way...")
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
["git", "apply", "--3way", str(patch_path)],
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
cwd=project_root,
|
|
271
|
+
)
|
|
272
|
+
return result.returncode == 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@click.group()
|
|
276
|
+
@click.version_option(package_name="alephprover")
|
|
277
|
+
def main():
|
|
278
|
+
"""Aleph Prover CLI — submit Lean proof requests."""
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@main.command()
|
|
282
|
+
@click.argument("file_path")
|
|
283
|
+
@click.argument("theorem_name")
|
|
284
|
+
@click.option("--hints", default=None, help="Hints for the prover")
|
|
285
|
+
@click.option("--time-budget", default=None, type=int, help="Time budget in minutes (default: server-side 900)")
|
|
286
|
+
@click.option("--cost-budget", default=None, type=float, help="Cost budget in USD (default: server-side 50)")
|
|
287
|
+
@click.option("-v", "--verbose", is_flag=True, help="List files included in the archive")
|
|
288
|
+
@click.option("--all-files", is_flag=True, help="Archive ALL project files (may include sensitive data)")
|
|
289
|
+
def prove(
|
|
290
|
+
file_path: str,
|
|
291
|
+
theorem_name: str,
|
|
292
|
+
hints: str | None,
|
|
293
|
+
time_budget: int | None,
|
|
294
|
+
cost_budget: float | None,
|
|
295
|
+
verbose: bool,
|
|
296
|
+
all_files: bool,
|
|
297
|
+
):
|
|
298
|
+
"""Submit a Lean theorem for proving.
|
|
299
|
+
|
|
300
|
+
FILE_PATH is the path to the Lean file (relative to project root).
|
|
301
|
+
THEOREM_NAME is the name of the theorem to prove.
|
|
302
|
+
"""
|
|
303
|
+
api_key = validate_api_key()
|
|
304
|
+
api_url = get_api_url()
|
|
305
|
+
|
|
306
|
+
# Find project root
|
|
307
|
+
if Path(file_path).is_absolute():
|
|
308
|
+
start = Path(file_path)
|
|
309
|
+
else:
|
|
310
|
+
start = Path.cwd()
|
|
311
|
+
project_root = find_project_root(start)
|
|
312
|
+
click.echo(f"Project root: {project_root}")
|
|
313
|
+
|
|
314
|
+
# Resolve file_path relative to project root
|
|
315
|
+
abs_file = (project_root / file_path) if not Path(file_path).is_absolute() else Path(file_path)
|
|
316
|
+
if not abs_file.exists():
|
|
317
|
+
click.echo(f"Error: File not found: {abs_file}", err=True)
|
|
318
|
+
sys.exit(1)
|
|
319
|
+
rel_file = abs_file.relative_to(project_root).as_posix()
|
|
320
|
+
|
|
321
|
+
# Zip
|
|
322
|
+
click.echo("Zipping project...")
|
|
323
|
+
zip_path = zip_project(project_root, verbose=verbose, all_files=all_files)
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Submit
|
|
327
|
+
click.echo("Submitting proof request...")
|
|
328
|
+
request_id = submit(api_url, api_key, zip_path, rel_file, theorem_name, hints, time_budget, cost_budget)
|
|
329
|
+
click.echo(f"Request ID: {request_id}")
|
|
330
|
+
click.echo(f"View at: {api_url}/requests/{request_id}")
|
|
331
|
+
|
|
332
|
+
# Poll
|
|
333
|
+
click.echo("Waiting for proof...")
|
|
334
|
+
status = poll_status(api_url, api_key, request_id)
|
|
335
|
+
|
|
336
|
+
if status != "completed":
|
|
337
|
+
click.echo(f"Proof request {status}.", err=True)
|
|
338
|
+
click.echo(f"Details: {api_url}/requests/{request_id}", err=True)
|
|
339
|
+
sys.exit(1)
|
|
340
|
+
|
|
341
|
+
click.echo("Proof completed!")
|
|
342
|
+
|
|
343
|
+
# Download diff
|
|
344
|
+
click.echo("Downloading diff...")
|
|
345
|
+
patch_path = download_diff(api_url, api_key, request_id)
|
|
346
|
+
|
|
347
|
+
if patch_path is None:
|
|
348
|
+
click.echo(
|
|
349
|
+
"Could not download diff. Check results at:\n"
|
|
350
|
+
f" {api_url}/requests/{request_id}",
|
|
351
|
+
err=True,
|
|
352
|
+
)
|
|
353
|
+
sys.exit(1)
|
|
354
|
+
|
|
355
|
+
# Apply
|
|
356
|
+
click.echo("Applying patch...")
|
|
357
|
+
if apply_patch(patch_path, project_root):
|
|
358
|
+
click.echo("Proof applied successfully!")
|
|
359
|
+
# Show what changed
|
|
360
|
+
result = subprocess.run(["git", "diff", "--stat"], capture_output=True, text=True, cwd=project_root)
|
|
361
|
+
if result.stdout:
|
|
362
|
+
click.echo(result.stdout)
|
|
363
|
+
# Cleanup patch
|
|
364
|
+
patch_path.unlink(missing_ok=True)
|
|
365
|
+
else:
|
|
366
|
+
click.echo(
|
|
367
|
+
f"Could not apply patch automatically.\n"
|
|
368
|
+
f"Patch saved to: {patch_path}\n"
|
|
369
|
+
f"Apply manually with: git apply {patch_path}",
|
|
370
|
+
err=True,
|
|
371
|
+
)
|
|
372
|
+
sys.exit(1)
|
|
373
|
+
finally:
|
|
374
|
+
# Cleanup zip
|
|
375
|
+
Path(zip_path).unlink(missing_ok=True)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@main.command("install-skill")
|
|
379
|
+
@click.option("--global", "global_", is_flag=True, help="Install to ~/.claude/commands/ instead of project")
|
|
380
|
+
def install_skill(global_: bool):
|
|
381
|
+
"""Install the /prove skill for Claude Code."""
|
|
382
|
+
if global_:
|
|
383
|
+
target = Path.home() / ".claude" / "commands" / "prove.md"
|
|
384
|
+
else:
|
|
385
|
+
target = Path.cwd() / ".claude" / "commands" / "prove.md"
|
|
386
|
+
|
|
387
|
+
source = importlib.resources.files("alephprover") / "prove.md"
|
|
388
|
+
content = source.read_text(encoding="utf-8")
|
|
389
|
+
|
|
390
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
391
|
+
target.write_text(content, encoding="utf-8")
|
|
392
|
+
click.echo(f"Installed /prove skill to {target}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Submit a Lean theorem to Aleph Prover and apply the proof
|
|
3
|
+
allowed-tools: Bash(uvx:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Aleph Prover Skill
|
|
7
|
+
|
|
8
|
+
Submit a Lean theorem to the Aleph Prover API using the `alephprover` CLI.
|
|
9
|
+
|
|
10
|
+
## Arguments
|
|
11
|
+
|
|
12
|
+
The user provides the theorem name and file path. Examples:
|
|
13
|
+
- `/prove mul_left_cancel in Mathlib/Algebra/Group/Basic.lean`
|
|
14
|
+
- `/prove my_theorem in MyProject/Basic.lean with hint: try induction on n`
|
|
15
|
+
|
|
16
|
+
Parse the user's message to extract:
|
|
17
|
+
- `theorem_name` (required): the theorem to prove
|
|
18
|
+
- `file_path` (required): path to the Lean file containing the theorem
|
|
19
|
+
- `hints` (optional): any hints or guidance after "with hint:" or "hint:"
|
|
20
|
+
|
|
21
|
+
If the theorem name or file path is unclear, ask the user to clarify before proceeding.
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
**Required:** `PROVER_API_KEY` environment variable (starts with `sk-aleph-`)
|
|
26
|
+
|
|
27
|
+
**Optional:** `PROVER_API_URL` environment variable (defaults to `https://alephprover.logicalintelligence.com`)
|
|
28
|
+
|
|
29
|
+
## Run
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uvx alephprover prove <file_path> <theorem_name> [--hints "..."] [--time-budget <minutes>] [--cost-budget <usd>] [-v]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `timeout=3700000` (just over 60 min) for the command since polling can take up to 60 minutes.
|
|
36
|
+
|
|
37
|
+
## After completion
|
|
38
|
+
|
|
39
|
+
1. Show whether the proof was applied successfully
|
|
40
|
+
2. Show which files were modified (`git diff --stat`)
|
|
41
|
+
3. Read the modified theorem from the file and show the proof to the user
|