lean-runtime 4.22.0__tar.gz → 4.23.0__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.
Files changed (197) hide show
  1. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/CHANGELOG.md +2 -0
  2. {lean_runtime-4.22.0/lean_runtime.egg-info → lean_runtime-4.23.0}/PKG-INFO +1 -1
  3. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/cli.py +78 -13
  4. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/engine.py +109 -2
  5. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/oci.py +11 -0
  6. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/project_execution.py +66 -2
  7. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/run_cli.py +12 -6
  8. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/runtime.py +13 -0
  9. {lean_runtime-4.22.0 → lean_runtime-4.23.0/lean_runtime.egg-info}/PKG-INFO +1 -1
  10. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime.egg-info/SOURCES.txt +1 -0
  11. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/pyproject.toml +1 -1
  12. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_engine.py +106 -1
  13. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_bundles.py +3 -0
  14. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_cli.py +226 -1
  15. lean_runtime-4.23.0/tests/test_project_bootstrap.py +106 -0
  16. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_run_cli.py +28 -0
  17. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/LICENSE +0 -0
  18. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/MANIFEST.in +0 -0
  19. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/README.md +0 -0
  20. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/environments.toml +0 -0
  21. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
  22. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
  23. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
  24. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
  25. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
  26. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  27. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  28. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  29. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
  30. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/catalog/locks/mathlib-v4.33.1.lock.json +0 -0
  31. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/compatibility/README.md +0 -0
  32. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
  33. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/compatibility/mathlib-4.32.2.json +0 -0
  34. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/compatibility/mathlib-4.32.2.toml +0 -0
  35. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/compatibility/mathlib-4.33.0.json +0 -0
  36. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/compatibility/mathlib-4.33.0.toml +0 -0
  37. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/concepts/context-selection.md +0 -0
  38. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/index.md +0 -0
  39. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/reference/commands.md +0 -0
  40. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/reference/python-advanced.md +0 -0
  41. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/reference/python-api.md +0 -0
  42. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/reference/storage.md +0 -0
  43. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/trust.md +0 -0
  44. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/tutorial.md +0 -0
  45. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/workflows/check-files.md +0 -0
  46. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/docs/workflows/lake-projects.md +0 -0
  47. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/examples/mathlib.toml +0 -0
  48. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/__init__.py +0 -0
  49. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/__main__.py +0 -0
  50. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/_git.py +0 -0
  51. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/_paths.py +0 -0
  52. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/backends.py +0 -0
  53. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/bundles.py +0 -0
  54. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/capsules.py +0 -0
  55. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/comparison.py +0 -0
  56. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/console.py +0 -0
  57. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/context_resolution.py +0 -0
  58. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/decisions.py +0 -0
  59. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/declaration_index.py +0 -0
  60. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/declaration_index_build.py +0 -0
  61. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/declaration_index_oci.py +0 -0
  62. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/diagnostics.py +0 -0
  63. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/__init__.py +0 -0
  64. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/analyzer.py +0 -0
  65. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/api.py +0 -0
  66. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/candidate.py +0 -0
  67. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/catalog.py +0 -0
  68. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/catalog_build.py +0 -0
  69. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/catalog_cli.py +0 -0
  70. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
  71. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/data/catalog.json +0 -0
  72. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/defaults.py +0 -0
  73. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/errors.py +0 -0
  74. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/history.py +0 -0
  75. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/memo.py +0 -0
  76. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/module_inventory.py +0 -0
  77. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/planner.py +0 -0
  78. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/policy.py +0 -0
  79. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/probe.py +0 -0
  80. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/py.typed +0 -0
  81. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/result.py +0 -0
  82. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/schema_resources.py +0 -0
  83. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  84. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  85. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  86. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/environments.py +0 -0
  87. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/errors.py +0 -0
  88. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/events.py +0 -0
  89. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/facade.py +0 -0
  90. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/frontmatter.py +0 -0
  91. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/header_cache.py +0 -0
  92. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/health.py +0 -0
  93. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/identifier_resolver.py +0 -0
  94. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/import_syntax.py +0 -0
  95. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/lake.py +0 -0
  96. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/lake_cache.py +0 -0
  97. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/lockfiles.py +0 -0
  98. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/locking.py +0 -0
  99. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/matrix.py +0 -0
  100. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/models.py +0 -0
  101. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/oci_protocol.py +0 -0
  102. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/packs.py +0 -0
  103. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/policies.py +0 -0
  104. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/profiling.py +0 -0
  105. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/programs.py +0 -0
  106. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/project_sharing.py +0 -0
  107. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/projects.py +0 -0
  108. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/publisher_verification.py +0 -0
  109. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/py.typed +0 -0
  110. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/references.py +0 -0
  111. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/resolver.py +0 -0
  112. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/schema_resources.py +0 -0
  113. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/serialization.py +0 -0
  114. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/shared_projects.py +0 -0
  115. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/specs.py +0 -0
  116. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/store.py +0 -0
  117. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/timings.py +0 -0
  118. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/toolchain_oci.py +0 -0
  119. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/toolchain_slim.py +0 -0
  120. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/toolchains.py +0 -0
  121. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/verification.py +0 -0
  122. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime/wire.py +0 -0
  123. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
  124. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime.egg-info/entry_points.txt +0 -0
  125. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime.egg-info/requires.txt +0 -0
  126. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/lean_runtime.egg-info/top_level.txt +0 -0
  127. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/mkdocs.yml +0 -0
  128. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/attestation-v1.schema.json +0 -0
  129. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/check-batch-v1.schema.json +0 -0
  130. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/cleanup-v1.schema.json +0 -0
  131. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/comparison-v1.schema.json +0 -0
  132. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/execution-v1.schema.json +0 -0
  133. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/inspect-v1.schema.json +0 -0
  134. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/matrix-v1.schema.json +0 -0
  135. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/plan-v1.schema.json +0 -0
  136. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/profile-v1.schema.json +0 -0
  137. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/publication-v1.schema.json +0 -0
  138. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/schemas/verify-v1.schema.json +0 -0
  139. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/announcement_gate.py +0 -0
  140. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/catalog_update.py +0 -0
  141. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/lake_cache_gate.py +0 -0
  142. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/prepare_declaration_index.py +0 -0
  143. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/publish_declaration_index.py +0 -0
  144. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/registry_preflight.py +0 -0
  145. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/run_compatibility.py +0 -0
  146. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/run_v1_case_study.py +0 -0
  147. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/scripts/smoke_wheel.py +0 -0
  148. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/setup.cfg +0 -0
  149. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/conftest.py +0 -0
  150. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
  151. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_analyzer.py +0 -0
  152. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_catalog.py +0 -0
  153. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_catalog_build.py +0 -0
  154. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
  155. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_memo.py +0 -0
  156. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_planner.py +0 -0
  157. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/discovery/test_policy.py +0 -0
  158. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_announcement_gate.py +0 -0
  159. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_backend.py +0 -0
  160. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_capsules.py +0 -0
  161. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_console.py +0 -0
  162. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_context_resolution.py +0 -0
  163. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_declaration_index.py +0 -0
  164. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_diagnostics.py +0 -0
  165. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_environment_integration.py +0 -0
  166. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_events_health.py +0 -0
  167. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_facade.py +0 -0
  168. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_frontmatter.py +0 -0
  169. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_git.py +0 -0
  170. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_header_cache.py +0 -0
  171. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_identifier_resolver.py +0 -0
  172. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_interactive.py +0 -0
  173. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_lake_cache.py +0 -0
  174. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_lockfiles.py +0 -0
  175. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_locking.py +0 -0
  176. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_models.py +0 -0
  177. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_multifile.py +0 -0
  178. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_oci_protocol.py +0 -0
  179. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_packs.py +0 -0
  180. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_paths.py +0 -0
  181. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_prebuilt_policy.py +0 -0
  182. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_programs.py +0 -0
  183. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_project_publish.py +0 -0
  184. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_projects.py +0 -0
  185. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_references.py +0 -0
  186. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_runtime.py +0 -0
  187. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_schema_resources.py +0 -0
  188. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_schemas.py +0 -0
  189. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_signatures.py +0 -0
  190. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_specs.py +0 -0
  191. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_store.py +0 -0
  192. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_toolchain_oci.py +0 -0
  193. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_toolchain_slim.py +0 -0
  194. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_toolchains.py +0 -0
  195. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_v1_precision.py +0 -0
  196. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_verification_inventory.py +0 -0
  197. {lean_runtime-4.22.0 → lean_runtime-4.23.0}/tests/test_verification_probe.py +0 -0
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 4.23.0 - 2026-08-24
6
+
5
7
  ## 4.22.0 - 2026-08-23
6
8
 
7
9
  ## 4.21.0 - 2026-08-23
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 4.22.0
3
+ Version: 4.23.0
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -119,18 +119,20 @@ def _render_storage(status: StoreStatus) -> None:
119
119
  ("Executions", status.executions, status.executions_bytes),
120
120
  ("Scratch", status.scratch_workspaces, status.scratch_bytes),
121
121
  )
122
+ label_width = max(len(label) for label, _count, _bytes in rows)
123
+ count_width = max(8, *(len(str(count)) for _label, count, _bytes in rows if count is not None))
122
124
  for label, count, bytes_used in rows:
123
- counted = f"{count:>5}" if count is not None else " "
125
+ counted = f"{count:>{count_width}}" if count is not None else " " * count_width
124
126
  size_column = style.cyan(f"{format_byte_size(bytes_used):>10}")
125
- print(f" {label:<15}{counted} {size_column}")
126
- total_label = style.bold(f"{'Materialized':<15}")
127
+ print(f" {label:<{label_width}} {counted} {size_column}")
128
+ total_label = style.bold(f"{'Materialized':<{label_width}}")
127
129
  total_size = style.bold(f"{format_byte_size(status.bytes_used):>10}")
128
130
  free_note = style.dim(f"({format_byte_size(status.bytes_free)} free on disk)")
129
- print(f" {total_label} {total_size} {free_note}")
131
+ print(f" {total_label} {' ' * count_width} {total_size} {free_note}")
130
132
  if status.allocated_bytes:
131
- allocated_label = style.bold(f"{'Allocated*':<15}")
133
+ allocated_label = style.bold(f"{'Allocated*':<{label_width}}")
132
134
  allocated_size = style.bold(f"{format_byte_size(status.allocated_bytes):>10}")
133
- print(f" {allocated_label} {allocated_size}")
135
+ print(f" {allocated_label} {' ' * count_width} {allocated_size}")
134
136
  print(style.dim(" * hard links counted once; copy-on-write blocks may still be shared"))
135
137
  if status.environment_usage:
136
138
  print()
@@ -886,6 +888,13 @@ Advanced namespaces:
886
888
  adopt.add_argument(
887
889
  "--dry-run", action="store_true", help="show the adoption plan without changing anything"
888
890
  )
891
+ adopt.add_argument(
892
+ "--no-agents",
893
+ dest="agents",
894
+ action="store_false",
895
+ default=True,
896
+ help="skip generating AGENTS.md",
897
+ )
889
898
  _add_common_output(adopt)
890
899
  adopt.set_defaults(command="attach", recursive=None, execute=False)
891
900
 
@@ -1150,6 +1159,13 @@ Advanced namespaces:
1150
1159
  pshare.add_argument(
1151
1160
  "--dry-run", action="store_true", help="show the sharing plan without changing anything"
1152
1161
  )
1162
+ pshare.add_argument(
1163
+ "--no-agents",
1164
+ dest="agents",
1165
+ action="store_false",
1166
+ default=True,
1167
+ help="skip generating AGENTS.md",
1168
+ )
1153
1169
  _add_common_output(pshare)
1154
1170
  pshare.set_defaults(command="attach", recursive=None, execute=False)
1155
1171
  punshare = pc.add_parser("unshare", help="materialize independent dependency copies")
@@ -1679,9 +1695,19 @@ def main(argv: list[str] | None = None) -> int:
1679
1695
  if args.agents:
1680
1696
  print(f"Agent guide: {init_result.root / 'AGENTS.md'}")
1681
1697
  project_name = init_plan.project_name or init_result.root.name
1682
- print(
1683
- f"Next: cd {init_result.root} && lean-runtime check {project_name}/Basic.lean"
1698
+ # Lake capitalizes the module directory during `lake init`, so derive
1699
+ # the hint from the file it actually created rather than the raw name.
1700
+ created = sorted(
1701
+ path
1702
+ for path in init_result.root.glob("*/Basic.lean")
1703
+ if not path.parent.name.startswith(".")
1684
1704
  )
1705
+ module_file = (
1706
+ created[0].relative_to(init_result.root)
1707
+ if created
1708
+ else Path(project_name) / "Basic.lean"
1709
+ )
1710
+ print(f"Next: cd {init_result.root} && lean-runtime check {module_file}")
1685
1711
  return 0
1686
1712
  if args.command == "scan":
1687
1713
  scan_result = runtime.scan_projects(args.path, recursive=args.recursive)
@@ -1793,8 +1819,24 @@ def main(argv: list[str] | None = None) -> int:
1793
1819
  recursive=args.recursive,
1794
1820
  plan=adoption_plan,
1795
1821
  )
1822
+ # Adoption is the canonical moment a repository becomes agent-ready,
1823
+ # including re-runs against already-attached projects.
1824
+ agent_guides = (
1825
+ [
1826
+ guide
1827
+ for attached in adoption_result.results
1828
+ if (guide := runtime.write_agents_guide(attached.root)) is not None
1829
+ ]
1830
+ if args.agents
1831
+ else []
1832
+ )
1796
1833
  if args.json:
1797
- _json(adoption_result.to_dict())
1834
+ _json(
1835
+ {
1836
+ **adoption_result.to_dict(),
1837
+ "agent_guides": [str(guide) for guide in agent_guides],
1838
+ }
1839
+ )
1798
1840
  else:
1799
1841
  _render_adoption_plan(adoption_result.plan)
1800
1842
  for attached in adoption_result.results:
@@ -1802,6 +1844,8 @@ def main(argv: list[str] | None = None) -> int:
1802
1844
  f"{attached.action}: {attached.root} · "
1803
1845
  f"{format_byte_size(attached.reclaimed_bytes)} replaced"
1804
1846
  )
1847
+ for guide in agent_guides:
1848
+ print(f"Agent guide: {guide}")
1805
1849
  for root, message in adoption_result.failures:
1806
1850
  print(f"failed: {root}: {message}", file=sys.stderr)
1807
1851
  return 0 if adoption_result.ok else 1
@@ -1958,6 +2002,10 @@ def main(argv: list[str] | None = None) -> int:
1958
2002
  name=args.name,
1959
2003
  build_timeout=args.timeout,
1960
2004
  accelerate=args.accelerate,
2005
+ # Acquire means the whole environment: demanding the root
2006
+ # module keeps a sparse capsule from satisfying it with an
2007
+ # empty closure that only looks ready.
2008
+ import_roots=(ROOT_MODULE,),
1961
2009
  )
1962
2010
  _json(environment.inspect().to_dict())
1963
2011
  if args.timings:
@@ -2252,12 +2300,22 @@ def main(argv: list[str] | None = None) -> int:
2252
2300
  "toolchain": project_status.toolchain,
2253
2301
  "manifest": str(project_status.manifest) if project_status.manifest else None,
2254
2302
  "attached": (project_status.root / "lean-runtime.toml").is_file(),
2303
+ "agents_guide": (
2304
+ str(project_status.root / "AGENTS.md")
2305
+ if (project_status.root / "AGENTS.md").is_file()
2306
+ else None
2307
+ ),
2255
2308
  }
2256
2309
  if args.json:
2257
2310
  _json(status_payload)
2258
2311
  else:
2259
- print(f"{status_payload['kind'].title()}")
2260
- for key, value in status_payload.items():
2312
+ display_payload = dict(status_payload)
2313
+ if display_payload.get("kind") == "project" and (
2314
+ display_payload.get("agents_guide") is None
2315
+ ):
2316
+ display_payload["agents_guide"] = "missing (`lean-runtime adopt` writes one)"
2317
+ print(f"{display_payload['kind'].title()}")
2318
+ for key, value in display_payload.items():
2261
2319
  if key == "kind":
2262
2320
  continue
2263
2321
  label = key.replace("_", " ").title()
@@ -2547,7 +2605,7 @@ def main(argv: list[str] | None = None) -> int:
2547
2605
  watched = Path(args.inputs[0]).expanduser().resolve()
2548
2606
  if not watched.is_file():
2549
2607
  raise ValueError(f"watched Lean file does not exist: {watched}")
2550
- print(f"Watching {watched} · Ctrl-C to stop")
2608
+ print(f"Watching {watched} · Ctrl-C to stop", flush=True)
2551
2609
  if os.environ.get(_HEADER_SNAPSHOTS_VARIABLE) is None:
2552
2610
  runtime.header_cache.enabled = True
2553
2611
  previous: tuple[int, int] | None = None
@@ -2563,6 +2621,7 @@ def main(argv: list[str] | None = None) -> int:
2563
2621
  policy=_policy(args),
2564
2622
  )
2565
2623
  _emit_result(watched_result, False)
2624
+ sys.stdout.flush()
2566
2625
  time.sleep(args.watch_interval)
2567
2626
  check_subject = (
2568
2627
  Path(args.inputs[-1]).name
@@ -2614,10 +2673,16 @@ def main(argv: list[str] | None = None) -> int:
2614
2673
  if args.include:
2615
2674
  raise ValueError("local project checks do not accept --include")
2616
2675
  display_path = "<stdin>"
2676
+ stdin_project = args.project
2677
+ if stdin_project is None and args.toolchain is None:
2678
+ try:
2679
+ stdin_project = discover_project(Path.cwd()).root
2680
+ except ProjectNotFoundError:
2681
+ stdin_project = None
2617
2682
  result = runtime.check(
2618
2683
  sys.stdin.read(),
2619
2684
  toolchain=args.toolchain,
2620
- project=args.project,
2685
+ project=stdin_project,
2621
2686
  policy=_policy(args),
2622
2687
  )
2623
2688
  source_file = None
@@ -2,11 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import threading
6
7
  import time
7
8
 
8
9
  from ..errors import LeanRuntimeError
9
- from .candidate import DiscoveryPlan
10
+ from ..models import ExecutionResult
11
+ from .candidate import Candidate, DiscoveryPlan
12
+ from .catalog import CatalogEntry
10
13
  from .probe import CandidateProbe, ProbeIntegrityFailure, ProbeUnavailable
11
14
  from .result import (
12
15
  AttemptStatus,
@@ -27,6 +30,8 @@ DISCOVERY_EXHAUSTED = "DISCOVERY_EXHAUSTED"
27
30
  DISCOVERY_EXPLICIT_LOCK = "DISCOVERY_EXPLICIT_LOCK"
28
31
  DISCOVERY_CANCELLED = "DISCOVERY_CANCELLED"
29
32
  DISCOVERY_RUNTIME_ERROR = "DISCOVERY_RUNTIME_ERROR"
33
+ DISCOVERY_MODULE_UNAVAILABLE = "DISCOVERY_MODULE_UNAVAILABLE"
34
+ DISCOVERY_VERDICT_BOUNDED = "DISCOVERY_VERDICT_BOUNDED"
30
35
  CANDIDATE_LEAN_REJECTED = "CANDIDATE_LEAN_REJECTED"
31
36
  CANDIDATE_UNAVAILABLE = "CANDIDATE_UNAVAILABLE"
32
37
  CANDIDATE_INTEGRITY_FAILURE = "CANDIDATE_INTEGRITY_FAILURE"
@@ -145,6 +150,63 @@ def _rejection_detail(stderr: str, stdout: str) -> str:
145
150
  return stderr.strip() or stdout.strip() or "Lean rejected the source"
146
151
 
147
152
 
153
+ # Lean reports a missing import as `unknown module prefix 'X'` (directory-shaped
154
+ # roots) or `unknown module 'X'`; both are decidable against candidate
155
+ # inventories without running another compiler.
156
+ _UNKNOWN_MODULE = re.compile(r"unknown module(?: prefix)? '([^']+)'")
157
+ # Identifier-level failures are the rejection class where an older environment
158
+ # legitimately changes the verdict (renamed or deleted lemmas), so they always
159
+ # justify continuing the candidate march.
160
+ _IDENTIFIER_MARKERS = ("unknown identifier", "unknown constant")
161
+ # Positive evidence that the source itself is the problem. Kept deliberately
162
+ # narrow: anything unrecognized preserves the ordinary march.
163
+ _PROOF_LEVEL_MARKERS = ("type mismatch", "unsolved goals", "unexpected token")
164
+
165
+
166
+ def _provides_module(entry: CatalogEntry, name: str) -> bool:
167
+ return name in entry.modules or any(module.startswith(name + ".") for module in entry.modules)
168
+
169
+
170
+ def _rejection_verdict(
171
+ result: ExecutionResult, candidates: tuple[Candidate, ...]
172
+ ) -> tuple[str, tuple[str, ...]]:
173
+ """Classify whether another candidate environment could change a rejection.
174
+
175
+ Returns one of:
176
+ - ``("module_unavailable", names)``: the source imports module roots that no
177
+ planned candidate provides; every further attempt must fail identically.
178
+ - ``("source_verdict", ())``: a proof-level failure (type mismatch,
179
+ unsolved goals, syntax); a second opinion never justifies a fresh
180
+ download — only local environments or an offline plan's source builds.
181
+ - ``("march", ())``: everything else — identifier-level failures, modules
182
+ another candidate does provide, and unclassifiable output all keep the
183
+ ordinary bounded march.
184
+ """
185
+ text = "\n".join(
186
+ (
187
+ *(item.message for item in result.diagnostics),
188
+ result.stderr,
189
+ result.stdout,
190
+ )
191
+ )
192
+ missing = tuple(dict.fromkeys(_UNKNOWN_MODULE.findall(text)))
193
+ if missing:
194
+ unavailable = tuple(
195
+ name
196
+ for name in missing
197
+ if not any(_provides_module(candidate.entry, name) for candidate in candidates)
198
+ )
199
+ if unavailable:
200
+ return "module_unavailable", unavailable
201
+ return "march", ()
202
+ lowered = text.lower()
203
+ if any(marker in lowered for marker in _IDENTIFIER_MARKERS):
204
+ return "march", ()
205
+ if any(marker in lowered for marker in _PROOF_LEVEL_MARKERS):
206
+ return "source_verdict", ()
207
+ return "march", ()
208
+
209
+
148
210
  def discover(
149
211
  source: str,
150
212
  plan: DiscoveryPlan,
@@ -189,6 +251,11 @@ def discover(
189
251
  raise ValueError("an authoritative candidate probe is required")
190
252
  attempts: list[CandidateAttempt] = []
191
253
  remote_acquisitions = 0
254
+ # Set once a proof-level rejection shows the source itself is the problem:
255
+ # remaining candidates are only worth a look when reaching them costs no
256
+ # download (already local, or an offline plan's deliberate source builds).
257
+ local_candidates_only = False
258
+ verdict_bounded_skips = 0
192
259
  for candidate in plan.planned_candidates:
193
260
  if cancel is not None and cancel.is_set():
194
261
  return DiscoveryResult(
@@ -217,6 +284,12 @@ def discover(
217
284
  candidate_is_local = any(
218
285
  reason.code == "RANK_LOCAL_AVAILABLE" for reason in candidate.reasons
219
286
  )
287
+ # A proof-level rejection never justifies a *download*. Offline plans
288
+ # (allow_download=False) acquire candidates by deliberate local source
289
+ # builds, so their march is preserved in full.
290
+ if local_candidates_only and not candidate_is_local and plan.policy.allow_download:
291
+ verdict_bounded_skips += 1
292
+ continue
220
293
  if not candidate_is_local and remote_acquisitions >= plan.policy.max_remote_acquisitions:
221
294
  return DiscoveryResult(
222
295
  status="not_found",
@@ -427,6 +500,29 @@ def discover(
427
500
  duration_seconds=time.monotonic() - started,
428
501
  diagnostics=attempt.diagnostics,
429
502
  )
503
+ if outcome_status == "lean_rejected":
504
+ verdict, unavailable = _rejection_verdict(result, plan.planned_candidates)
505
+ if verdict == "module_unavailable":
506
+ # Statically decidable: no candidate can change this verdict.
507
+ return DiscoveryResult(
508
+ status="not_found",
509
+ confidence="exhausted",
510
+ outcome="source_rejected",
511
+ completion="complete",
512
+ plan=plan,
513
+ attempts=tuple(attempts),
514
+ duration_seconds=time.monotonic() - started,
515
+ diagnostics=(
516
+ ResultDiagnostic(
517
+ DISCOVERY_MODULE_UNAVAILABLE,
518
+ "no cataloged environment provides module(s): "
519
+ + ", ".join(unavailable),
520
+ ),
521
+ *attempt.diagnostics,
522
+ ),
523
+ )
524
+ if verdict == "source_verdict":
525
+ local_candidates_only = True
430
526
 
431
527
  if time.monotonic() >= wall_deadline or probe_remaining <= 0:
432
528
  reason = ResultDiagnostic(DISCOVERY_TIME_LIMIT, "total wall-clock budget expired")
@@ -465,6 +561,17 @@ def discover(
465
561
  if attempt.status == "timeout"
466
562
  for diagnostic in attempt.diagnostics
467
563
  )
564
+ skip_diagnostics = (
565
+ (
566
+ ResultDiagnostic(
567
+ DISCOVERY_VERDICT_BOUNDED,
568
+ f"skipped {verdict_bounded_skips} non-local candidate(s): a proof-level "
569
+ "rejection does not justify downloading further environments",
570
+ ),
571
+ )
572
+ if verdict_bounded_skips
573
+ else ()
574
+ )
468
575
  return DiscoveryResult(
469
576
  status="not_found",
470
577
  confidence=confidence,
@@ -473,5 +580,5 @@ def discover(
473
580
  plan=plan,
474
581
  attempts=tuple(attempts),
475
582
  duration_seconds=time.monotonic() - started,
476
- diagnostics=(*timeout_diagnostics, reason, *rejection_diagnostics),
583
+ diagnostics=(*timeout_diagnostics, reason, *skip_diagnostics, *rejection_diagnostics),
477
584
  )
@@ -887,6 +887,17 @@ class OCIEnvironmentCache:
887
887
  capsule = CapsuleManifest.from_dict(config["capsule"])
888
888
  if capsule.lock_id != lock.lock_id or capsule.toolchain != lock.toolchain:
889
889
  raise EnvironmentError("downloadable capsule does not match its lock")
890
+ # A capsule that lacks a requested root cannot satisfy this acquisition;
891
+ # silently projecting a partial closure would yield an environment that
892
+ # claims readiness while missing the modules the caller asked for.
893
+ available = {module.name for module in capsule.modules}
894
+ unavailable = tuple(root for root in roots if root not in available)
895
+ if unavailable:
896
+ raise DownloadUnavailable(
897
+ "downloadable capsule does not provide module(s): " + ", ".join(unavailable),
898
+ reason_code="capsule_roots_unavailable",
899
+ retryable=False,
900
+ )
890
901
  closure = capsule.closure(roots)
891
902
  module_names = tuple(module.name for module in closure)
892
903
  descriptors = {
@@ -9,6 +9,7 @@ its artifact cache without changing the public ``Runtime`` API.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import json
12
13
  import os
13
14
  import re
14
15
  import subprocess
@@ -17,6 +18,7 @@ import tempfile
17
18
  import threading
18
19
  import time
19
20
  from collections.abc import Callable, Sequence
21
+ from contextlib import AbstractContextManager, nullcontext
20
22
  from dataclasses import replace
21
23
  from pathlib import Path
22
24
  from typing import TYPE_CHECKING
@@ -28,6 +30,7 @@ else: # pragma: no cover - exercised by the Python 3.10 CI job
28
30
 
29
31
  from .errors import ProjectError
30
32
  from .import_syntax import IMPORT_STATEMENT
33
+ from .locking import FileLock
31
34
  from .models import ExecutionResult, PhaseTiming
32
35
  from .policies import ExecutionPolicy
33
36
  from .project_sharing import project_sharing_enabled
@@ -91,6 +94,63 @@ class ProjectExecutor:
91
94
  def __init__(self, runtime: Runtime) -> None:
92
95
  self.runtime = runtime
93
96
 
97
+ @staticmethod
98
+ def _declares_dependencies(context: ProjectContext) -> bool:
99
+ """Whether the lakefile requires packages Lake would have to fetch."""
100
+ try:
101
+ text = context.lakefile.read_text(encoding="utf-8")
102
+ except OSError:
103
+ return False
104
+ if context.lakefile.name == "lakefile.toml":
105
+ try:
106
+ return bool(tomllib.loads(text).get("require"))
107
+ except tomllib.TOMLDecodeError:
108
+ return False
109
+ return any(line.lstrip().startswith("require ") for line in text.splitlines())
110
+
111
+ @staticmethod
112
+ def _packages_materialized(root: Path, manifest: Path) -> bool:
113
+ """Whether every manifest package already has a checkout on disk."""
114
+ try:
115
+ document = json.loads(manifest.read_text(encoding="utf-8"))
116
+ except (OSError, ValueError):
117
+ return True # let Lake report the manifest problem itself
118
+ packages = document.get("packages")
119
+ if not isinstance(packages, list):
120
+ return True
121
+ packages_dir = root / str(document.get("packagesDir", ".lake/packages"))
122
+ for package in packages:
123
+ name = package.get("name") if isinstance(package, dict) else None
124
+ if isinstance(name, str) and name and not (packages_dir / name).exists():
125
+ return False
126
+ return True
127
+
128
+ def _bootstrap_guard(self, context: ProjectContext) -> AbstractContextManager[object]:
129
+ """Never let a check implicitly resolve dependencies, and never in parallel.
130
+
131
+ A missing manifest with declared dependencies means ``lake env`` would
132
+ run an implicit ``lake update`` — cloning and compiling dependencies
133
+ from source, which is Lake's job to do once, deliberately. Concurrent
134
+ checks against a manifest whose packages are not yet materialized race
135
+ Lake's checkout of the same directories, so exactly one check may run
136
+ until the packages exist.
137
+ """
138
+ manifest = context.current_manifest()
139
+ if manifest is None:
140
+ if self._declares_dependencies(context):
141
+ raise ProjectError(
142
+ f"{context.root} declares dependencies but has no lake-manifest.json; "
143
+ "run `lake update` once to resolve versions, then `lean-runtime build` "
144
+ "to restore shared dependency artifacts"
145
+ )
146
+ elif self._packages_materialized(context.root, manifest):
147
+ return nullcontext()
148
+ return FileLock(
149
+ context.root / ".lake" / "lean-runtime" / "bootstrap.lock",
150
+ timeout=3600,
151
+ owner={"operation": "project_bootstrap", "root": str(context.root)},
152
+ )
153
+
94
154
  def _checked_with_header_snapshots(
95
155
  self,
96
156
  context: ProjectContext,
@@ -162,7 +222,8 @@ class ProjectExecutor:
162
222
  context, provenance.workspace_digest, relative, text, command, execute, cancel
163
223
  )
164
224
 
165
- result = self._build_missing_local_import(context, text, run(), run, policy, cancel)
225
+ with self._bootstrap_guard(context):
226
+ result = self._build_missing_local_import(context, text, run(), run, policy, cancel)
166
227
  return self._with_identifier_hints(context, result)
167
228
 
168
229
  def check_source(
@@ -213,7 +274,10 @@ class ProjectExecutor:
213
274
  cancel,
214
275
  )
215
276
 
216
- result = self._build_missing_local_import(context, source, run(), run, policy, cancel)
277
+ with self._bootstrap_guard(context):
278
+ result = self._build_missing_local_import(
279
+ context, source, run(), run, policy, cancel
280
+ )
217
281
  return self._with_identifier_hints(context, result)
218
282
 
219
283
  def _build_missing_local_import(
@@ -199,7 +199,12 @@ def _discovery_failure(result: DiscoveryResult) -> str:
199
199
  return (details[0] if details else "no compatible environment was found") + suffix
200
200
 
201
201
 
202
- def _discovery_summary(result: DiscoveryResult) -> str:
202
+ def _discovery_summary(result: DiscoveryResult) -> str | None:
203
+ # A finished search that ends in a compiler rejection needs no epilogue:
204
+ # an environment was found and the verdict is the diagnostic above. Only
205
+ # an *incomplete* search (a limit stopped it) is worth a trailing line.
206
+ if result.outcome == "source_rejected" and result.completion == "complete":
207
+ return None
203
208
  tested = len(result.attempts)
204
209
  planned = len(getattr(result.plan, "planned_candidates", result.plan.candidates))
205
210
  untried = max(0, planned - tested)
@@ -592,10 +597,13 @@ def run(
592
597
  )
593
598
  if context_resolution.kind in {"lock", "toolchain", "project", "discovery"}:
594
599
  result = replace(result, timings=(preparation, *result.timings))
595
- if rejected_discovery is not None:
600
+ discovery_summary = (
601
+ _discovery_summary(rejected_discovery) if rejected_discovery is not None else None
602
+ )
603
+ if discovery_summary is not None:
596
604
  result = replace(
597
605
  result,
598
- hints=(*result.hints, _discovery_summary(rejected_discovery)),
606
+ hints=(*result.hints, discovery_summary),
599
607
  )
600
608
  renderer.close()
601
609
  _emit(
@@ -607,9 +615,7 @@ def run(
607
615
  summary_elapsed_seconds=(
608
616
  rejected_discovery.duration_seconds if rejected_discovery is not None else None
609
617
  ),
610
- discovery_summary=(
611
- _discovery_summary(rejected_discovery) if rejected_discovery is not None else None
612
- ),
618
+ discovery_summary=discovery_summary,
613
619
  )
614
620
  if result.ok:
615
621
  return 0
@@ -1605,6 +1605,19 @@ class Runtime:
1605
1605
  failures.append((project.root, str(exc)))
1606
1606
  return AdoptionBatchResult(selected_plan, tuple(results), tuple(failures))
1607
1607
 
1608
+ def write_agents_guide(self, root: str | os.PathLike[str]) -> Path | None:
1609
+ """Write the default AGENTS.md into a project root.
1610
+
1611
+ Returns the created path, or ``None`` when a guide already exists —
1612
+ an existing guide is the project's own voice and is never replaced.
1613
+ """
1614
+ target = Path(root).expanduser().resolve()
1615
+ guide = target / "AGENTS.md"
1616
+ if guide.exists():
1617
+ return None
1618
+ guide.write_text(_DEFAULT_AGENTS_GUIDE, encoding="utf-8")
1619
+ return guide
1620
+
1608
1621
  def scan_projects(
1609
1622
  self, path: str | os.PathLike[str], *, recursive: bool = True
1610
1623
  ) -> ProjectScanResult:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 4.22.0
3
+ Version: 4.23.0
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -167,6 +167,7 @@ tests/test_packs.py
167
167
  tests/test_paths.py
168
168
  tests/test_prebuilt_policy.py
169
169
  tests/test_programs.py
170
+ tests/test_project_bootstrap.py
170
171
  tests/test_project_publish.py
171
172
  tests/test_projects.py
172
173
  tests/test_references.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "4.22.0"
7
+ version = "4.23.0"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"