@paradigma-inc/flywheel 0.1.19 → 0.1.26
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.
- package/README.md +8 -0
- package/package.json +1 -1
- package/skills/flywheel/references/experiment-design-protocol.md +1 -1
- package/skills/flywheel/references/flywheel-mcp-tool-map.md +32 -35
- package/skills/flywheel/setting-up-flywheel/updating-flywheel-mcp.md +8 -0
- package/skills/flywheel-auto/SKILL.md +1 -1
- package/skills/flywheel-auto/references/ARTIFACTS.md +0 -1
- package/skills/flywheel-auto/references/INTERFACES.md +16 -10
- package/skills/flywheel-auto/references/experiment-design-protocol.md +1 -1
- package/skills/flywheel-auto/references/flywheel-mcp-tool-map.md +32 -35
- package/skills/flywheel-lookahead/SKILL.md +22 -15
- package/skills/flywheel-lookahead/agents/openai.yaml +3 -3
- package/skills/flywheel-lookahead/evals/evals.json +13 -1
- package/skills/flywheel-lookahead/references/ARTIFACTS.md +0 -1
- package/skills/flywheel-lookahead/references/INTERFACES.md +16 -10
- package/skills/flywheel-lookahead/references/flywheel-mcp-tool-map.md +32 -35
- package/skills/flywheel-prove/SKILL.md +163 -0
- package/skills/flywheel-prove/agents/interface.yaml +4 -0
- package/skills/flywheel-prove/assets/pipeline_template/bin/tproof +3 -0
- package/skills/flywheel-prove/assets/pipeline_template/bin/tproof.cmd +2 -0
- package/skills/flywheel-prove/assets/pipeline_template/logs/.gitkeep +1 -0
- package/skills/flywheel-prove/assets/pipeline_template/pyproject.toml +23 -0
- package/skills/flywheel-prove/assets/pipeline_template/scripts/smoke_test.cmd +2 -0
- package/skills/flywheel-prove/assets/pipeline_template/scripts/smoke_test.sh +3 -0
- package/skills/flywheel-prove/assets/pipeline_template/src/tproof/__init__.py +1 -0
- package/skills/flywheel-prove/assets/pipeline_template/src/tproof/cli.py +298 -0
- package/skills/flywheel-prove/assets/pipeline_template/src/tproof/constants.py +10 -0
- package/skills/flywheel-prove/assets/pipeline_template/src/tproof/layout.py +51 -0
- package/skills/flywheel-prove/assets/pipeline_template/src/tproof/leanops.py +116 -0
- package/skills/flywheel-prove/assets/pipeline_template/src/tproof/runstore.py +58 -0
- package/skills/flywheel-prove/assets/pipeline_template/src/tproof/tasking.py +94 -0
- package/skills/flywheel-prove/assets/pipeline_template/workspace/prompts/fill_sorries.txt +3 -0
- package/skills/flywheel-prove/references/workflow.md +193 -0
- package/skills/flywheel-prove/scripts/scaffold_pipeline.py +111 -0
- package/skills/flywheel-reproduce/SKILL.md +28 -23
- package/skills/flywheel-reproduce/evals/evals.json +7 -1
- package/skills/flywheel-reproduce/references/ARTIFACTS.md +0 -1
- package/skills/flywheel-reproduce/references/INTERFACES.md +16 -10
- package/skills/flywheel-reproduce/references/experiment-design-protocol.md +1 -1
- package/skills/flywheel-reproduce/references/flywheel-mcp-tool-map.md +32 -35
- package/skills/flywheel-reproduce/references/source-blog.md +35 -0
- package/skills/flywheel-reproduce/references/source-generic.md +30 -0
- package/skills/flywheel-reproduce/references/source-notes.md +35 -0
- package/skills/flywheel-reproduce/references/source-paper.md +89 -0
- package/skills/flywheel-reproduce/references/source-wiki.md +36 -0
- package/skills/flywheel-to-graph/SKILL.md +26 -21
- package/skills/flywheel-to-graph/evals/evals.json +7 -1
- package/skills/flywheel-to-graph/references/ARTIFACTS.md +0 -1
- package/skills/flywheel-to-graph/references/INTERFACES.md +16 -10
- package/skills/flywheel-to-graph/references/flywheel-mcp-tool-map.md +32 -35
- package/skills/flywheel-to-graph/references/source-blog.md +34 -0
- package/skills/flywheel-to-graph/references/source-generic.md +29 -0
- package/skills/flywheel-to-graph/references/source-notes.md +34 -0
- package/skills/flywheel-to-graph/references/source-paper.md +85 -0
- package/skills/flywheel-to-graph/references/source-wiki.md +35 -0
- package/src/cli.mjs +69 -3
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
|
|
8
|
+
from tproof.constants import (
|
|
9
|
+
LEAN_TOOLCHAIN,
|
|
10
|
+
MATHLIB_HASH,
|
|
11
|
+
OPS_AGENT_NOTES,
|
|
12
|
+
OPS_AGENT_ROLE,
|
|
13
|
+
PACKAGE_NAME,
|
|
14
|
+
PROOF_AGENT_NOTES,
|
|
15
|
+
PROOF_AGENT_ROLE,
|
|
16
|
+
)
|
|
17
|
+
from tproof.layout import Paths, default_paths, ensure_layout
|
|
18
|
+
from tproof.leanops import (
|
|
19
|
+
ensure_toolchain_installed,
|
|
20
|
+
lake_build,
|
|
21
|
+
lake_update,
|
|
22
|
+
reindex_allproofs,
|
|
23
|
+
seed_lean_project,
|
|
24
|
+
snapshot_project,
|
|
25
|
+
)
|
|
26
|
+
from tproof.runstore import create_run, load_run, new_run_id, now_utc_iso, save_run
|
|
27
|
+
from tproof.tasking import (
|
|
28
|
+
build_run_brief,
|
|
29
|
+
discover_project_modules,
|
|
30
|
+
find_sorry_hits,
|
|
31
|
+
read_task_prompt,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
app = typer.Typer(no_args_is_help=True, help="Local Lean theorem proving pipeline CLI")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _paths() -> Paths:
|
|
38
|
+
paths = default_paths()
|
|
39
|
+
ensure_layout(paths)
|
|
40
|
+
return paths
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _write_log(log_path: Path, text: str) -> None:
|
|
44
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
log_path.write_text(text, encoding="utf-8")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_forbidden_staging_artifacts(paths: Paths) -> list[str]:
|
|
49
|
+
staging_dir = paths.workspace_dir / "staging"
|
|
50
|
+
if not staging_dir.is_dir():
|
|
51
|
+
return []
|
|
52
|
+
blocked_suffixes = {".lean", ".md"}
|
|
53
|
+
hits: list[str] = []
|
|
54
|
+
for file_path in sorted(staging_dir.rglob("*")):
|
|
55
|
+
if not file_path.is_file():
|
|
56
|
+
continue
|
|
57
|
+
if file_path.name == ".gitkeep":
|
|
58
|
+
continue
|
|
59
|
+
if file_path.suffix.lower() in blocked_suffixes:
|
|
60
|
+
hits.append(str(file_path.relative_to(paths.root)))
|
|
61
|
+
return hits
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _which(binary: str) -> str | None:
|
|
65
|
+
return shutil.which(binary)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("doctor")
|
|
69
|
+
def doctor() -> None:
|
|
70
|
+
paths = _paths()
|
|
71
|
+
lean_path = _which("lean")
|
|
72
|
+
lake_path = _which("lake")
|
|
73
|
+
elan_path = _which("elan")
|
|
74
|
+
|
|
75
|
+
if not lean_path or not lake_path or not elan_path:
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
"Missing required binaries. Ensure `lean`, `lake`, and `elan` are installed and on PATH."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
from tproof.leanops import run_cmd
|
|
81
|
+
|
|
82
|
+
lean_code, lean_out = run_cmd(["lean", "--version"], cwd=paths.root)
|
|
83
|
+
lake_code, lake_out = run_cmd(["lake", "--version"], cwd=paths.root)
|
|
84
|
+
if lean_code != 0 or lake_code != 0:
|
|
85
|
+
raise RuntimeError("Unable to run `lean --version` or `lake --version`.")
|
|
86
|
+
|
|
87
|
+
rprint(f"[green]lean:[/green] {lean_path}")
|
|
88
|
+
rprint(f"[green]lake:[/green] {lake_path}")
|
|
89
|
+
rprint(f"[green]elan:[/green] {elan_path}")
|
|
90
|
+
rprint(f"[green]Pinned toolchain:[/green] {LEAN_TOOLCHAIN}")
|
|
91
|
+
rprint(f"[green]Pinned mathlib hash:[/green] {MATHLIB_HASH}")
|
|
92
|
+
rprint(f"[green]Proof worker:[/green] {PROOF_AGENT_ROLE}")
|
|
93
|
+
rprint(f"[green]Proof notes:[/green] {PROOF_AGENT_NOTES}")
|
|
94
|
+
rprint(f"[green]Ops worker:[/green] {OPS_AGENT_ROLE}")
|
|
95
|
+
rprint(f"[green]Ops notes:[/green] {OPS_AGENT_NOTES}")
|
|
96
|
+
rprint(f"[green]Lean project path:[/green] {paths.lean_project_dir}")
|
|
97
|
+
rprint(lean_out.strip())
|
|
98
|
+
rprint(lake_out.strip())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("init")
|
|
102
|
+
def init(build: bool = typer.Option(True, "--build/--no-build")) -> None:
|
|
103
|
+
paths = _paths()
|
|
104
|
+
seed_lean_project(
|
|
105
|
+
paths.lean_project_dir,
|
|
106
|
+
lean_toolchain=LEAN_TOOLCHAIN,
|
|
107
|
+
mathlib_hash=MATHLIB_HASH,
|
|
108
|
+
)
|
|
109
|
+
reindex_allproofs(paths.lean_project_dir)
|
|
110
|
+
|
|
111
|
+
install_code, install_out = ensure_toolchain_installed(LEAN_TOOLCHAIN)
|
|
112
|
+
already_installed = "already installed" in install_out.lower()
|
|
113
|
+
if install_code != 0 and not already_installed:
|
|
114
|
+
raise RuntimeError(f"elan toolchain install failed:\n{install_out}")
|
|
115
|
+
|
|
116
|
+
update_code, update_out = lake_update(paths.lean_project_dir)
|
|
117
|
+
if update_code != 0:
|
|
118
|
+
raise RuntimeError(f"lake update failed:\n{update_out}")
|
|
119
|
+
|
|
120
|
+
logs = [
|
|
121
|
+
"== elan toolchain install ==",
|
|
122
|
+
install_out,
|
|
123
|
+
"== lake update ==",
|
|
124
|
+
update_out,
|
|
125
|
+
]
|
|
126
|
+
if build:
|
|
127
|
+
build_code, build_out = lake_build(paths.lean_project_dir)
|
|
128
|
+
if build_code != 0:
|
|
129
|
+
raise RuntimeError(f"lake build failed:\n{build_out}")
|
|
130
|
+
logs.extend(["== lake build ==", build_out])
|
|
131
|
+
|
|
132
|
+
log_name = datetime.now(timezone.utc).strftime("init_%Y%m%dT%H%M%SZ.log")
|
|
133
|
+
_write_log(paths.logs_dir / log_name, "\n".join(logs))
|
|
134
|
+
rprint(f"[green]Workspace initialized:[/green] {paths.lean_project_dir}")
|
|
135
|
+
rprint(f"[green]Toolchain:[/green] {LEAN_TOOLCHAIN}")
|
|
136
|
+
rprint(f"[green]Mathlib hash:[/green] {MATHLIB_HASH}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command("reindex")
|
|
140
|
+
def reindex(project_dir: Path | None = typer.Option(None, "--project-dir")) -> None:
|
|
141
|
+
paths = _paths()
|
|
142
|
+
target = project_dir or paths.lean_project_dir
|
|
143
|
+
all_proofs_path = reindex_allproofs(target)
|
|
144
|
+
rprint(f"[green]Reindexed:[/green] {all_proofs_path}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command("start-run")
|
|
148
|
+
def start_run(
|
|
149
|
+
prompt_file: Path | None = typer.Option(None, "--prompt-file"),
|
|
150
|
+
prompt_text: str | None = typer.Option(None, "--prompt-text"),
|
|
151
|
+
run_label: str | None = typer.Option(None, "--run-label"),
|
|
152
|
+
context_file: Path | None = typer.Option(None, "--context-file"),
|
|
153
|
+
) -> None:
|
|
154
|
+
paths = _paths()
|
|
155
|
+
task_prompt = read_task_prompt(prompt_file=prompt_file, prompt_text=prompt_text)
|
|
156
|
+
context_text = context_file.read_text(encoding="utf-8") if context_file else None
|
|
157
|
+
modules = discover_project_modules(paths.lean_project_dir)
|
|
158
|
+
sorry_hits = find_sorry_hits(paths.lean_project_dir)
|
|
159
|
+
|
|
160
|
+
run_id = new_run_id(run_label)
|
|
161
|
+
run = {
|
|
162
|
+
"run_id": run_id,
|
|
163
|
+
"created_at": now_utc_iso(),
|
|
164
|
+
"updated_at": now_utc_iso(),
|
|
165
|
+
"status": "READY_FOR_PROOF",
|
|
166
|
+
"task_prompt": task_prompt,
|
|
167
|
+
"context_file": str(context_file.resolve()) if context_file else None,
|
|
168
|
+
"context_text": context_text,
|
|
169
|
+
"known_modules": modules,
|
|
170
|
+
"sorry_hits": sorry_hits,
|
|
171
|
+
"proof_agent_role": PROOF_AGENT_ROLE,
|
|
172
|
+
"proof_agent_notes": PROOF_AGENT_NOTES,
|
|
173
|
+
"ops_agent_role": OPS_AGENT_ROLE,
|
|
174
|
+
"ops_agent_notes": OPS_AGENT_NOTES,
|
|
175
|
+
"lean_project_dir": str(paths.lean_project_dir),
|
|
176
|
+
}
|
|
177
|
+
create_run(paths, run_id, run)
|
|
178
|
+
|
|
179
|
+
run_dir = paths.runs_dir / run_id
|
|
180
|
+
snapshot_project(paths.lean_project_dir, run_dir / "project_snapshot")
|
|
181
|
+
(run_dir / "task_prompt.txt").write_text(task_prompt, encoding="utf-8")
|
|
182
|
+
if context_text:
|
|
183
|
+
(run_dir / "context.txt").write_text(context_text, encoding="utf-8")
|
|
184
|
+
brief = build_run_brief(
|
|
185
|
+
run_id=run_id,
|
|
186
|
+
task_prompt=task_prompt,
|
|
187
|
+
known_modules=modules,
|
|
188
|
+
sorry_hits=sorry_hits,
|
|
189
|
+
context_text=context_text,
|
|
190
|
+
)
|
|
191
|
+
(run_dir / "run_brief.md").write_text(brief, encoding="utf-8")
|
|
192
|
+
|
|
193
|
+
rprint(f"[green]Run created:[/green] {run_id}")
|
|
194
|
+
rprint(f"[green]Run file:[/green] {run_dir / 'run.json'}")
|
|
195
|
+
rprint(f"[green]Brief file:[/green] {run_dir / 'run_brief.md'}")
|
|
196
|
+
rprint(f"[green]Outstanding sorries:[/green] {len(sorry_hits)}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command("show-run")
|
|
200
|
+
def show_run(run_id: str = typer.Argument(...)) -> None:
|
|
201
|
+
paths = _paths()
|
|
202
|
+
run = load_run(paths, run_id)
|
|
203
|
+
rprint(run)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command("set-status")
|
|
207
|
+
def set_status(
|
|
208
|
+
run_id: str = typer.Argument(...),
|
|
209
|
+
status: str = typer.Argument(...),
|
|
210
|
+
note: str | None = typer.Option(None, "--note"),
|
|
211
|
+
) -> None:
|
|
212
|
+
paths = _paths()
|
|
213
|
+
run = load_run(paths, run_id)
|
|
214
|
+
run["status"] = status
|
|
215
|
+
run["updated_at"] = now_utc_iso()
|
|
216
|
+
if note:
|
|
217
|
+
run["note"] = note
|
|
218
|
+
save_run(paths, run_id, run)
|
|
219
|
+
rprint(f"[green]Updated run {run_id} -> {status}[/green]")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command("verify")
|
|
223
|
+
def verify(project_dir: Path | None = typer.Option(None, "--project-dir")) -> None:
|
|
224
|
+
paths = _paths()
|
|
225
|
+
target = project_dir or paths.lean_project_dir
|
|
226
|
+
code, output = lake_build(target)
|
|
227
|
+
sorry_hits = find_sorry_hits(target)
|
|
228
|
+
forbidden_staging_hits = _find_forbidden_staging_artifacts(paths)
|
|
229
|
+
log_name = datetime.now(timezone.utc).strftime("verify_%Y%m%dT%H%M%SZ.log")
|
|
230
|
+
log_path = paths.logs_dir / log_name
|
|
231
|
+
extra = ""
|
|
232
|
+
if sorry_hits:
|
|
233
|
+
extra = "\n\n== unresolved sorry locations ==\n" + "\n".join(sorry_hits)
|
|
234
|
+
if forbidden_staging_hits:
|
|
235
|
+
extra += "\n\n== forbidden artifacts under workspace/staging ==\n" + "\n".join(
|
|
236
|
+
forbidden_staging_hits
|
|
237
|
+
)
|
|
238
|
+
_write_log(log_path, output + extra)
|
|
239
|
+
|
|
240
|
+
if code != 0 or sorry_hits or forbidden_staging_hits:
|
|
241
|
+
rprint(f"[red]Verification failed.[/red] Log: {log_path}")
|
|
242
|
+
if sorry_hits:
|
|
243
|
+
rprint(f"[red]Unresolved sorries:[/red] {len(sorry_hits)}")
|
|
244
|
+
if forbidden_staging_hits:
|
|
245
|
+
rprint(
|
|
246
|
+
"[red]Forbidden staging artifacts:[/red] "
|
|
247
|
+
f"{len(forbidden_staging_hits)} (move them to ProofWorkspace/Final)"
|
|
248
|
+
)
|
|
249
|
+
raise typer.Exit(code=1)
|
|
250
|
+
rprint(f"[green]Verification passed.[/green] Log: {log_path}")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@app.command("smoke-test")
|
|
254
|
+
def smoke_test() -> None:
|
|
255
|
+
paths = _paths()
|
|
256
|
+
init(build=True)
|
|
257
|
+
|
|
258
|
+
smoke_dir = paths.lean_project_dir / PACKAGE_NAME / "Smoke"
|
|
259
|
+
smoke_dir.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
smoke_file = smoke_dir / "Smoke.lean"
|
|
261
|
+
smoke_file.write_text(
|
|
262
|
+
"""import Mathlib
|
|
263
|
+
|
|
264
|
+
namespace ProofWorkspace.Smoke
|
|
265
|
+
|
|
266
|
+
theorem smoke_add_comm (a b : Nat) : a + b = b + a := by
|
|
267
|
+
sorry
|
|
268
|
+
|
|
269
|
+
end ProofWorkspace.Smoke
|
|
270
|
+
""",
|
|
271
|
+
encoding="utf-8",
|
|
272
|
+
)
|
|
273
|
+
reindex_allproofs(paths.lean_project_dir)
|
|
274
|
+
|
|
275
|
+
run_id_label = "smoke"
|
|
276
|
+
start_run(
|
|
277
|
+
prompt_text="Fill all sorry placeholders while preserving theorem statements.",
|
|
278
|
+
run_label=run_id_label,
|
|
279
|
+
prompt_file=None,
|
|
280
|
+
context_file=None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
verify(project_dir=paths.lean_project_dir)
|
|
285
|
+
except typer.Exit:
|
|
286
|
+
rprint("[green]Smoke test passed:[/green] pipeline detects unresolved `sorry` correctly.")
|
|
287
|
+
return
|
|
288
|
+
raise RuntimeError(
|
|
289
|
+
"Smoke test expected verification failure before proving, but verify passed."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def main() -> None:
|
|
294
|
+
app()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
main()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
PACKAGE_NAME = "ProofWorkspace"
|
|
2
|
+
LEAN_TOOLCHAIN = "leanprover/lean4:v4.28.0"
|
|
3
|
+
MATHLIB_HASH = "8f9d9cff6bd728b17a24e163c9402775d9e6a365"
|
|
4
|
+
|
|
5
|
+
PROOF_AGENT_ROLE = "deep-thinking model for complex mathematics"
|
|
6
|
+
PROOF_AGENT_NOTES = (
|
|
7
|
+
"Use a high-capability reasoning model from the active agent stack for theorem proving."
|
|
8
|
+
)
|
|
9
|
+
OPS_AGENT_ROLE = "smaller programming model for scripting/reporting"
|
|
10
|
+
OPS_AGENT_NOTES = "Use a smaller coding-oriented model for automation, logs, and status reporting."
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class Paths:
|
|
7
|
+
root: Path
|
|
8
|
+
bin_dir: Path
|
|
9
|
+
scripts_dir: Path
|
|
10
|
+
src_dir: Path
|
|
11
|
+
workspace_dir: Path
|
|
12
|
+
prompts_dir: Path
|
|
13
|
+
contexts_dir: Path
|
|
14
|
+
lean_project_dir: Path
|
|
15
|
+
data_dir: Path
|
|
16
|
+
runs_dir: Path
|
|
17
|
+
logs_dir: Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_paths() -> Paths:
|
|
21
|
+
root = Path(__file__).resolve().parents[2]
|
|
22
|
+
workspace_dir = root / "workspace"
|
|
23
|
+
return Paths(
|
|
24
|
+
root=root,
|
|
25
|
+
bin_dir=root / "bin",
|
|
26
|
+
scripts_dir=root / "scripts",
|
|
27
|
+
src_dir=root / "src",
|
|
28
|
+
workspace_dir=workspace_dir,
|
|
29
|
+
prompts_dir=workspace_dir / "prompts",
|
|
30
|
+
contexts_dir=workspace_dir / "contexts",
|
|
31
|
+
lean_project_dir=workspace_dir / "lean_project",
|
|
32
|
+
data_dir=root / "data",
|
|
33
|
+
runs_dir=root / "data" / "runs",
|
|
34
|
+
logs_dir=root / "logs",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def ensure_layout(paths: Paths) -> None:
|
|
39
|
+
for directory in (
|
|
40
|
+
paths.bin_dir,
|
|
41
|
+
paths.scripts_dir,
|
|
42
|
+
paths.src_dir,
|
|
43
|
+
paths.workspace_dir,
|
|
44
|
+
paths.prompts_dir,
|
|
45
|
+
paths.contexts_dir,
|
|
46
|
+
paths.lean_project_dir,
|
|
47
|
+
paths.data_dir,
|
|
48
|
+
paths.runs_dir,
|
|
49
|
+
paths.logs_dir,
|
|
50
|
+
):
|
|
51
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from tproof.constants import PACKAGE_NAME
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_cmd(command: list[str], cwd: Path) -> tuple[int, str]:
|
|
9
|
+
proc = subprocess.run(
|
|
10
|
+
command,
|
|
11
|
+
cwd=cwd,
|
|
12
|
+
capture_output=True,
|
|
13
|
+
text=True,
|
|
14
|
+
check=False,
|
|
15
|
+
)
|
|
16
|
+
output = (proc.stdout or "") + (proc.stderr or "")
|
|
17
|
+
return proc.returncode, output
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ensure_toolchain_installed(toolchain: str) -> tuple[int, str]:
|
|
21
|
+
return run_cmd(["elan", "toolchain", "install", toolchain], cwd=Path.cwd())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def lake_update(project_dir: Path) -> tuple[int, str]:
|
|
25
|
+
return run_cmd(["lake", "update"], cwd=project_dir)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def lake_build(project_dir: Path) -> tuple[int, str]:
|
|
29
|
+
return run_cmd(["lake", "build"], cwd=project_dir)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def seed_lean_project(project_dir: Path, lean_toolchain: str, mathlib_hash: str) -> None:
|
|
33
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
package_dir = project_dir / PACKAGE_NAME
|
|
35
|
+
package_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
(package_dir / "Demo").mkdir(parents=True, exist_ok=True)
|
|
37
|
+
(package_dir / "Final").mkdir(parents=True, exist_ok=True)
|
|
38
|
+
(project_dir / "lean-toolchain").write_text(f"{lean_toolchain}\n", encoding="utf-8")
|
|
39
|
+
(project_dir / "lakefile.lean").write_text(
|
|
40
|
+
f"""import Lake
|
|
41
|
+
open Lake DSL
|
|
42
|
+
|
|
43
|
+
package "{PACKAGE_NAME}"
|
|
44
|
+
|
|
45
|
+
require mathlib from git
|
|
46
|
+
"https://github.com/leanprover-community/mathlib4.git" @ "{mathlib_hash}"
|
|
47
|
+
|
|
48
|
+
@[default_target]
|
|
49
|
+
lean_lib "{PACKAGE_NAME}" where
|
|
50
|
+
""",
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
53
|
+
(project_dir / f"{PACKAGE_NAME}.lean").write_text(
|
|
54
|
+
f"""import {PACKAGE_NAME}.AllProofs
|
|
55
|
+
""",
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
)
|
|
58
|
+
all_proofs = project_dir / PACKAGE_NAME / "AllProofs.lean"
|
|
59
|
+
if not all_proofs.is_file():
|
|
60
|
+
all_proofs.write_text(
|
|
61
|
+
"""/-
|
|
62
|
+
Auto-generated import index for proof modules.
|
|
63
|
+
Regenerate with: tproof reindex
|
|
64
|
+
-/
|
|
65
|
+
""",
|
|
66
|
+
encoding="utf-8",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
placeholder = project_dir / PACKAGE_NAME / "Demo" / "Placeholder.lean"
|
|
70
|
+
if not placeholder.is_file():
|
|
71
|
+
placeholder.write_text(
|
|
72
|
+
f"""namespace {PACKAGE_NAME}.Demo
|
|
73
|
+
|
|
74
|
+
theorem placeholder : True := by
|
|
75
|
+
trivial
|
|
76
|
+
|
|
77
|
+
end {PACKAGE_NAME}.Demo
|
|
78
|
+
""",
|
|
79
|
+
encoding="utf-8",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def reindex_allproofs(project_dir: Path) -> Path:
|
|
84
|
+
package_dir = project_dir / PACKAGE_NAME
|
|
85
|
+
modules: list[str] = []
|
|
86
|
+
if package_dir.is_dir():
|
|
87
|
+
for lean_file in sorted(package_dir.rglob("*.lean")):
|
|
88
|
+
rel = lean_file.relative_to(project_dir).with_suffix("")
|
|
89
|
+
module_name = ".".join(rel.parts)
|
|
90
|
+
if module_name.endswith(".AllProofs"):
|
|
91
|
+
continue
|
|
92
|
+
modules.append(module_name)
|
|
93
|
+
|
|
94
|
+
all_proofs_path = package_dir / "AllProofs.lean"
|
|
95
|
+
lines = [
|
|
96
|
+
"/-",
|
|
97
|
+
"Auto-generated import index for proof modules.",
|
|
98
|
+
"Regenerate with: tproof reindex",
|
|
99
|
+
"-/",
|
|
100
|
+
"",
|
|
101
|
+
]
|
|
102
|
+
for module in modules:
|
|
103
|
+
lines.append(f"import {module}")
|
|
104
|
+
lines.append("")
|
|
105
|
+
all_proofs_path.write_text("\n".join(lines), encoding="utf-8")
|
|
106
|
+
return all_proofs_path
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def snapshot_project(source_project_dir: Path, destination_dir: Path) -> None:
|
|
110
|
+
if destination_dir.exists():
|
|
111
|
+
shutil.rmtree(destination_dir)
|
|
112
|
+
shutil.copytree(
|
|
113
|
+
source_project_dir,
|
|
114
|
+
destination_dir,
|
|
115
|
+
ignore=shutil.ignore_patterns(".lake", "build", "__pycache__", "*.olean", "*.ilean"),
|
|
116
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import secrets
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tproof.layout import Paths
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def now_utc_iso() -> str:
|
|
12
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _sanitize_label(label: str) -> str:
|
|
16
|
+
cleaned = re.sub(r"[^a-zA-Z0-9_-]+", "-", label.strip()).strip("-")
|
|
17
|
+
return cleaned[:40] if cleaned else "run"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def new_run_id(label: str | None = None) -> str:
|
|
21
|
+
prefix = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
22
|
+
token = secrets.token_hex(2)
|
|
23
|
+
if label:
|
|
24
|
+
return f"{prefix}_{_sanitize_label(label)}_{token}"
|
|
25
|
+
return f"{prefix}_{token}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_dir(paths: Paths, run_id: str) -> Path:
|
|
29
|
+
return paths.runs_dir / run_id
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_file(paths: Paths, run_id: str) -> Path:
|
|
33
|
+
return run_dir(paths, run_id) / "run.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
37
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_json(path: Path) -> dict[str, Any]:
|
|
42
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_run(paths: Paths, run_id: str, payload: dict[str, Any]) -> None:
|
|
46
|
+
run_dir(paths, run_id).mkdir(parents=True, exist_ok=True)
|
|
47
|
+
write_json(run_file(paths, run_id), payload)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_run(paths: Paths, run_id: str) -> dict[str, Any]:
|
|
51
|
+
path = run_file(paths, run_id)
|
|
52
|
+
if not path.is_file():
|
|
53
|
+
raise FileNotFoundError(f"Unknown run id '{run_id}'. Missing file: {path}")
|
|
54
|
+
return read_json(path)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def save_run(paths: Paths, run_id: str, payload: dict[str, Any]) -> None:
|
|
58
|
+
write_json(run_file(paths, run_id), payload)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from tproof.constants import (
|
|
5
|
+
OPS_AGENT_NOTES,
|
|
6
|
+
OPS_AGENT_ROLE,
|
|
7
|
+
PACKAGE_NAME,
|
|
8
|
+
PROOF_AGENT_NOTES,
|
|
9
|
+
PROOF_AGENT_ROLE,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_task_prompt(prompt_file: Path | None, prompt_text: str | None) -> str:
|
|
14
|
+
if prompt_file and prompt_text:
|
|
15
|
+
raise ValueError("Pass either --prompt-file or --prompt-text, not both.")
|
|
16
|
+
if prompt_file:
|
|
17
|
+
return prompt_file.read_text(encoding="utf-8").strip()
|
|
18
|
+
if prompt_text:
|
|
19
|
+
return prompt_text.strip()
|
|
20
|
+
raise ValueError("Pass one of --prompt-file or --prompt-text.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def discover_project_modules(project_dir: Path) -> list[str]:
|
|
24
|
+
package_dir = project_dir / PACKAGE_NAME
|
|
25
|
+
if not package_dir.is_dir():
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
modules: list[str] = []
|
|
29
|
+
for lean_file in sorted(package_dir.rglob("*.lean")):
|
|
30
|
+
rel = lean_file.relative_to(project_dir).with_suffix("")
|
|
31
|
+
module_name = ".".join(rel.parts)
|
|
32
|
+
if module_name.endswith(".AllProofs"):
|
|
33
|
+
continue
|
|
34
|
+
modules.append(module_name)
|
|
35
|
+
return modules
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_sorry_hits(project_dir: Path) -> list[str]:
|
|
39
|
+
word = re.compile(r"\bsorry\b")
|
|
40
|
+
hits: list[str] = []
|
|
41
|
+
package_dir = project_dir / PACKAGE_NAME
|
|
42
|
+
if not package_dir.is_dir():
|
|
43
|
+
return hits
|
|
44
|
+
|
|
45
|
+
for lean_file in sorted(package_dir.rglob("*.lean")):
|
|
46
|
+
rel = lean_file.relative_to(project_dir)
|
|
47
|
+
lines = lean_file.read_text(encoding="utf-8").splitlines()
|
|
48
|
+
for line_number, line in enumerate(lines, start=1):
|
|
49
|
+
if word.search(line):
|
|
50
|
+
hits.append(f"{rel}:{line_number}")
|
|
51
|
+
return hits
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_run_brief(
|
|
55
|
+
*,
|
|
56
|
+
run_id: str,
|
|
57
|
+
task_prompt: str,
|
|
58
|
+
known_modules: list[str],
|
|
59
|
+
sorry_hits: list[str],
|
|
60
|
+
context_text: str | None,
|
|
61
|
+
) -> str:
|
|
62
|
+
module_block = "\n".join(f"- {module}" for module in known_modules) or "- (none)"
|
|
63
|
+
sorry_block = "\n".join(f"- {hit}" for hit in sorry_hits) or "- (none)"
|
|
64
|
+
context_block = context_text.strip() if context_text else "(none)"
|
|
65
|
+
|
|
66
|
+
return f"""# Run Brief: {run_id}
|
|
67
|
+
|
|
68
|
+
## Task
|
|
69
|
+
{task_prompt}
|
|
70
|
+
|
|
71
|
+
## Context
|
|
72
|
+
{context_block}
|
|
73
|
+
|
|
74
|
+
## Known Modules
|
|
75
|
+
{module_block}
|
|
76
|
+
|
|
77
|
+
## Outstanding `sorry` Locations
|
|
78
|
+
{sorry_block}
|
|
79
|
+
|
|
80
|
+
## Model Routing (Required)
|
|
81
|
+
1. Mathematical proving worker:
|
|
82
|
+
- Profile: `{PROOF_AGENT_ROLE}`
|
|
83
|
+
- Guidance: {PROOF_AGENT_NOTES}
|
|
84
|
+
- Scope: theorem proving, proof search, lemma decomposition, difficult Lean terms.
|
|
85
|
+
2. Programming/ops/reporting worker:
|
|
86
|
+
- Profile: `{OPS_AGENT_ROLE}`
|
|
87
|
+
- Guidance: {OPS_AGENT_NOTES}
|
|
88
|
+
- Scope: scripts, repo hygiene, status summaries, verification logs, run metadata updates.
|
|
89
|
+
|
|
90
|
+
## Execution Pattern
|
|
91
|
+
1. Ask the proving worker to edit files under `workspace/lean_project/`.
|
|
92
|
+
2. Ask the ops worker to run `uv run -m tproof.cli verify` and summarize failures.
|
|
93
|
+
3. Iterate until verification passes with no `sorry`.
|
|
94
|
+
"""
|