leasehold 0.2.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 (269) hide show
  1. leasehold-0.2.0/.claude/settings.json +16 -0
  2. leasehold-0.2.0/.claude-plugin/marketplace.json +16 -0
  3. leasehold-0.2.0/.github/ISSUE_TEMPLATE/bug.yml +30 -0
  4. leasehold-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. leasehold-0.2.0/.github/ISSUE_TEMPLATE/feature.yml +15 -0
  6. leasehold-0.2.0/.github/workflows/ci.yml +60 -0
  7. leasehold-0.2.0/.github/workflows/pages.yml +37 -0
  8. leasehold-0.2.0/.github/workflows/release.yml +42 -0
  9. leasehold-0.2.0/.gitignore +8 -0
  10. leasehold-0.2.0/.importlinter +88 -0
  11. leasehold-0.2.0/.pre-commit-config.yaml +15 -0
  12. leasehold-0.2.0/AGENTS.md +40 -0
  13. leasehold-0.2.0/CHANGELOG.md +81 -0
  14. leasehold-0.2.0/CLAUDE.md +1 -0
  15. leasehold-0.2.0/CONTRIBUTING.md +48 -0
  16. leasehold-0.2.0/LICENSE +202 -0
  17. leasehold-0.2.0/PKG-INFO +163 -0
  18. leasehold-0.2.0/README.md +130 -0
  19. leasehold-0.2.0/SECURITY.md +52 -0
  20. leasehold-0.2.0/bench/job.py +120 -0
  21. leasehold-0.2.0/bench/probe.py +99 -0
  22. leasehold-0.2.0/bench/results/8cpu-16g/k4-full-d240.csv +10 -0
  23. leasehold-0.2.0/bench/results/8cpu-16g/k4-full-d240.md +21 -0
  24. leasehold-0.2.0/bench/results/8cpu-16g/k8-full-d240.csv +9 -0
  25. leasehold-0.2.0/bench/results/8cpu-16g/k8-full-d240.md +20 -0
  26. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory/jobs.csv +26 -0
  27. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory/outer.json +13 -0
  28. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory/run.json +14 -0
  29. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory/samples.csv.gz +0 -0
  30. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory-greedy/jobs.csv +26 -0
  31. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory-greedy/outer.json +13 -0
  32. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory-greedy/run.json +14 -0
  33. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-advisory-greedy/samples.csv.gz +0 -0
  34. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced/jobs.csv +26 -0
  35. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced/outer.json +13 -0
  36. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced/run.json +21 -0
  37. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced/samples.csv.gz +0 -0
  38. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced-greedy/jobs.csv +26 -0
  39. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced-greedy/outer.json +13 -0
  40. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced-greedy/run.json +21 -0
  41. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-enforced-greedy/samples.csv.gz +0 -0
  42. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-hardcpu-greedy/jobs.csv +26 -0
  43. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-hardcpu-greedy/outer.json +13 -0
  44. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-hardcpu-greedy/run.json +21 -0
  45. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/lh-hardcpu-greedy/samples.csv.gz +0 -0
  46. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/nice/jobs.csv +26 -0
  47. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/nice/outer.json +13 -0
  48. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/nice/run.json +14 -0
  49. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/nice/samples.csv.gz +0 -0
  50. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/slice/jobs.csv +26 -0
  51. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/slice/outer.json +13 -0
  52. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/slice/run.json +21 -0
  53. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/slice/samples.csv.gz +0 -0
  54. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/spec.json +163 -0
  55. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/static-jobs/jobs.csv +26 -0
  56. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/static-jobs/outer.json +13 -0
  57. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/static-jobs/run.json +14 -0
  58. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/static-jobs/samples.csv.gz +0 -0
  59. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/unmanaged/jobs.csv +26 -0
  60. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/unmanaged/outer.json +13 -0
  61. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/unmanaged/run.json +14 -0
  62. leasehold-0.2.0/bench/results/8cpu-16g/raw-k4-d240/unmanaged/samples.csv.gz +0 -0
  63. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-advisory/jobs.csv +53 -0
  64. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-advisory/outer.json +13 -0
  65. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-advisory/run.json +14 -0
  66. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-advisory/samples.csv.gz +0 -0
  67. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced/jobs.csv +53 -0
  68. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced/outer.json +13 -0
  69. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced/run.json +21 -0
  70. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced/samples.csv.gz +0 -0
  71. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced-greedy/jobs.csv +53 -0
  72. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced-greedy/outer.json +13 -0
  73. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced-greedy/run.json +21 -0
  74. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-enforced-greedy/samples.csv.gz +0 -0
  75. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-hardcpu-greedy/jobs.csv +53 -0
  76. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-hardcpu-greedy/outer.json +13 -0
  77. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-hardcpu-greedy/run.json +21 -0
  78. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/lh-hardcpu-greedy/samples.csv.gz +0 -0
  79. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/nice/jobs.csv +53 -0
  80. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/nice/outer.json +13 -0
  81. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/nice/run.json +14 -0
  82. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/nice/samples.csv.gz +0 -0
  83. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/slice/jobs.csv +53 -0
  84. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/slice/outer.json +13 -0
  85. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/slice/run.json +21 -0
  86. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/slice/samples.csv.gz +0 -0
  87. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/spec.json +278 -0
  88. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/static-jobs/jobs.csv +53 -0
  89. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/static-jobs/outer.json +13 -0
  90. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/static-jobs/run.json +14 -0
  91. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/static-jobs/samples.csv.gz +0 -0
  92. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/unmanaged/jobs.csv +53 -0
  93. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/unmanaged/outer.json +13 -0
  94. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/unmanaged/run.json +14 -0
  95. leasehold-0.2.0/bench/results/8cpu-16g/raw-k8-d240/unmanaged/samples.csv.gz +0 -0
  96. leasehold-0.2.0/bench/run.py +1019 -0
  97. leasehold-0.2.0/deploy/gates.slice +9 -0
  98. leasehold-0.2.0/deploy/github-runner/common.sh +25 -0
  99. leasehold-0.2.0/deploy/github-runner/job-completed.sh +19 -0
  100. leasehold-0.2.0/deploy/github-runner/job-started.sh +63 -0
  101. leasehold-0.2.0/deploy/github-runner/runner-slice.conf +6 -0
  102. leasehold-0.2.0/deploy/leasehold-report.service +33 -0
  103. leasehold-0.2.0/deploy/leasehold-user.service +18 -0
  104. leasehold-0.2.0/deploy/leasehold.service +35 -0
  105. leasehold-0.2.0/deploy/lend.slice +13 -0
  106. leasehold-0.2.0/deploy/machine.toml +90 -0
  107. leasehold-0.2.0/deploy/runner.slice +9 -0
  108. leasehold-0.2.0/deploy/work.slice +9 -0
  109. leasehold-0.2.0/docs/bench.md +246 -0
  110. leasehold-0.2.0/docs/index.html +688 -0
  111. leasehold-0.2.0/docs/media/top.gif +0 -0
  112. leasehold-0.2.0/docs/media/top.png +0 -0
  113. leasehold-0.2.0/docs/media/ui-dark.png +0 -0
  114. leasehold-0.2.0/docs/media/ui-light.png +0 -0
  115. leasehold-0.2.0/docs/reference.md +788 -0
  116. leasehold-0.2.0/examples/agents/README.md +105 -0
  117. leasehold-0.2.0/examples/agents/machine.toml +4 -0
  118. leasehold-0.2.0/examples/agents/run.sh +30 -0
  119. leasehold-0.2.0/examples/github-runner/README.md +110 -0
  120. leasehold-0.2.0/examples/github-runner/ephemeral.sh +18 -0
  121. leasehold-0.2.0/examples/github-runner/try-hooks.sh +17 -0
  122. leasehold-0.2.0/examples/lend/README.md +70 -0
  123. leasehold-0.2.0/examples/lend/batch-job.sh +8 -0
  124. leasehold-0.2.0/examples/lend/machine.toml +12 -0
  125. leasehold-0.2.0/examples/lend/run.sh +26 -0
  126. leasehold-0.2.0/examples/library/README.md +42 -0
  127. leasehold-0.2.0/examples/library/decide.py +51 -0
  128. leasehold-0.2.0/examples/observed/README.md +79 -0
  129. leasehold-0.2.0/examples/observed/machine.toml +15 -0
  130. leasehold-0.2.0/examples/observed/prep_dataset.py +9 -0
  131. leasehold-0.2.0/examples/observed/run.sh +24 -0
  132. leasehold-0.2.0/examples/test-suite/Makefile +9 -0
  133. leasehold-0.2.0/examples/test-suite/README.md +65 -0
  134. leasehold-0.2.0/examples/test-suite/job.sh +8 -0
  135. leasehold-0.2.0/examples/test-suite/run.sh +31 -0
  136. leasehold-0.2.0/examples/test-suite/test_sample.py +14 -0
  137. leasehold-0.2.0/integrations/AGENTS.md.snippet +15 -0
  138. leasehold-0.2.0/integrations/claude-code/.claude-plugin/plugin.json +16 -0
  139. leasehold-0.2.0/integrations/claude-code/hooks/hooks.json +40 -0
  140. leasehold-0.2.0/integrations/claude-code/hooks/prefilter.sh +62 -0
  141. leasehold-0.2.0/integrations/claude-code/skills/leasehold/SKILL.md +42 -0
  142. leasehold-0.2.0/integrations/mcp/README.md +128 -0
  143. leasehold-0.2.0/integrations/mcp/claude-code/.mcp.json +9 -0
  144. leasehold-0.2.0/integrations/mcp/codex/config.toml +4 -0
  145. leasehold-0.2.0/integrations/mcp/cursor/mcp.json +9 -0
  146. leasehold-0.2.0/integrations/mcp/gemini/settings.json +8 -0
  147. leasehold-0.2.0/pyproject.toml +74 -0
  148. leasehold-0.2.0/scripts/gen_cli_reference.py +69 -0
  149. leasehold-0.2.0/scripts/screenshots.py +253 -0
  150. leasehold-0.2.0/src/leasehold/__init__.py +18 -0
  151. leasehold-0.2.0/src/leasehold/__main__.py +8 -0
  152. leasehold-0.2.0/src/leasehold/access.py +43 -0
  153. leasehold-0.2.0/src/leasehold/admission.py +373 -0
  154. leasehold-0.2.0/src/leasehold/agent.py +570 -0
  155. leasehold-0.2.0/src/leasehold/api.py +129 -0
  156. leasehold-0.2.0/src/leasehold/busy.py +62 -0
  157. leasehold-0.2.0/src/leasehold/claude_code.py +218 -0
  158. leasehold-0.2.0/src/leasehold/cli/__init__.py +151 -0
  159. leasehold-0.2.0/src/leasehold/cli/admin.py +55 -0
  160. leasehold-0.2.0/src/leasehold/cli/agents.py +45 -0
  161. leasehold-0.2.0/src/leasehold/cli/daemon.py +169 -0
  162. leasehold-0.2.0/src/leasehold/cli/demo.py +64 -0
  163. leasehold-0.2.0/src/leasehold/cli/hooks.py +190 -0
  164. leasehold-0.2.0/src/leasehold/cli/leases.py +170 -0
  165. leasehold-0.2.0/src/leasehold/cli/push.py +42 -0
  166. leasehold-0.2.0/src/leasehold/cli/run.py +57 -0
  167. leasehold-0.2.0/src/leasehold/cli/setup.py +30 -0
  168. leasehold-0.2.0/src/leasehold/cli/views.py +214 -0
  169. leasehold-0.2.0/src/leasehold/cli/watch.py +49 -0
  170. leasehold-0.2.0/src/leasehold/client.py +86 -0
  171. leasehold-0.2.0/src/leasehold/commands.py +99 -0
  172. leasehold-0.2.0/src/leasehold/contain.py +279 -0
  173. leasehold-0.2.0/src/leasehold/demo.py +228 -0
  174. leasehold-0.2.0/src/leasehold/discovery.py +125 -0
  175. leasehold-0.2.0/src/leasehold/doctor.py +314 -0
  176. leasehold-0.2.0/src/leasehold/events.py +139 -0
  177. leasehold-0.2.0/src/leasehold/exits.py +16 -0
  178. leasehold-0.2.0/src/leasehold/facts.py +139 -0
  179. leasehold-0.2.0/src/leasehold/guard.py +88 -0
  180. leasehold-0.2.0/src/leasehold/handoff.py +190 -0
  181. leasehold-0.2.0/src/leasehold/hold.py +451 -0
  182. leasehold-0.2.0/src/leasehold/hooknotes.py +111 -0
  183. leasehold-0.2.0/src/leasehold/jobs.py +117 -0
  184. leasehold-0.2.0/src/leasehold/kinds.py +17 -0
  185. leasehold-0.2.0/src/leasehold/ledger.py +833 -0
  186. leasehold-0.2.0/src/leasehold/lend.py +101 -0
  187. leasehold-0.2.0/src/leasehold/load.py +131 -0
  188. leasehold-0.2.0/src/leasehold/machine.py +453 -0
  189. leasehold-0.2.0/src/leasehold/mcp_server.py +748 -0
  190. leasehold-0.2.0/src/leasehold/metrics.py +91 -0
  191. leasehold-0.2.0/src/leasehold/model.py +79 -0
  192. leasehold-0.2.0/src/leasehold/observe.py +174 -0
  193. leasehold-0.2.0/src/leasehold/paths.py +120 -0
  194. leasehold-0.2.0/src/leasehold/process.py +64 -0
  195. leasehold-0.2.0/src/leasehold/proctable.py +84 -0
  196. leasehold-0.2.0/src/leasehold/programs.py +55 -0
  197. leasehold-0.2.0/src/leasehold/py.typed +0 -0
  198. leasehold-0.2.0/src/leasehold/quota.py +44 -0
  199. leasehold-0.2.0/src/leasehold/report.py +190 -0
  200. leasehold-0.2.0/src/leasehold/server.py +190 -0
  201. leasehold-0.2.0/src/leasehold/session.py +131 -0
  202. leasehold-0.2.0/src/leasehold/setup.py +196 -0
  203. leasehold-0.2.0/src/leasehold/shell.py +312 -0
  204. leasehold-0.2.0/src/leasehold/shims.py +197 -0
  205. leasehold-0.2.0/src/leasehold/static/ui.html +1135 -0
  206. leasehold-0.2.0/src/leasehold/status.py +152 -0
  207. leasehold-0.2.0/src/leasehold/template.py +98 -0
  208. leasehold-0.2.0/src/leasehold/templates/machine.toml +88 -0
  209. leasehold-0.2.0/src/leasehold/templates/user.toml +60 -0
  210. leasehold-0.2.0/src/leasehold/terminal.py +87 -0
  211. leasehold-0.2.0/src/leasehold/top.py +1374 -0
  212. leasehold-0.2.0/src/leasehold/units.py +33 -0
  213. leasehold-0.2.0/src/leasehold/webui.py +159 -0
  214. leasehold-0.2.0/tests/conftest.py +36 -0
  215. leasehold-0.2.0/tests/fixtures/top/events-busy.json +100 -0
  216. leasehold-0.2.0/tests/fixtures/top/status-busy.json +291 -0
  217. leasehold-0.2.0/tests/fixtures/top/status-idle.json +155 -0
  218. leasehold-0.2.0/tests/golden/busy-100x40-ascii.styles +40 -0
  219. leasehold-0.2.0/tests/golden/busy-100x40-ascii.txt +40 -0
  220. leasehold-0.2.0/tests/golden/busy-100x40.styles +40 -0
  221. leasehold-0.2.0/tests/golden/busy-100x40.txt +40 -0
  222. leasehold-0.2.0/tests/golden/busy-80x24.styles +24 -0
  223. leasehold-0.2.0/tests/golden/busy-80x24.txt +24 -0
  224. leasehold-0.2.0/tests/golden/idle-100x40.styles +40 -0
  225. leasehold-0.2.0/tests/golden/idle-100x40.txt +40 -0
  226. leasehold-0.2.0/tests/support.py +129 -0
  227. leasehold-0.2.0/tests/test_access.py +49 -0
  228. leasehold-0.2.0/tests/test_admission.py +501 -0
  229. leasehold-0.2.0/tests/test_agent.py +216 -0
  230. leasehold-0.2.0/tests/test_api.py +140 -0
  231. leasehold-0.2.0/tests/test_bench.py +48 -0
  232. leasehold-0.2.0/tests/test_busy.py +60 -0
  233. leasehold-0.2.0/tests/test_claude_code.py +240 -0
  234. leasehold-0.2.0/tests/test_commands.py +166 -0
  235. leasehold-0.2.0/tests/test_contain.py +200 -0
  236. leasehold-0.2.0/tests/test_daemon.py +264 -0
  237. leasehold-0.2.0/tests/test_demo.py +31 -0
  238. leasehold-0.2.0/tests/test_discovery.py +90 -0
  239. leasehold-0.2.0/tests/test_docs.py +46 -0
  240. leasehold-0.2.0/tests/test_doctor.py +145 -0
  241. leasehold-0.2.0/tests/test_events.py +115 -0
  242. leasehold-0.2.0/tests/test_examples.py +275 -0
  243. leasehold-0.2.0/tests/test_facts_load.py +141 -0
  244. leasehold-0.2.0/tests/test_guard.py +106 -0
  245. leasehold-0.2.0/tests/test_handoff.py +172 -0
  246. leasehold-0.2.0/tests/test_hold.py +249 -0
  247. leasehold-0.2.0/tests/test_jobs.py +155 -0
  248. leasehold-0.2.0/tests/test_ledger.py +555 -0
  249. leasehold-0.2.0/tests/test_lend.py +147 -0
  250. leasehold-0.2.0/tests/test_machine.py +376 -0
  251. leasehold-0.2.0/tests/test_mcp.py +490 -0
  252. leasehold-0.2.0/tests/test_model.py +96 -0
  253. leasehold-0.2.0/tests/test_neutral.py +112 -0
  254. leasehold-0.2.0/tests/test_observe.py +336 -0
  255. leasehold-0.2.0/tests/test_paths.py +89 -0
  256. leasehold-0.2.0/tests/test_prefilter.py +158 -0
  257. leasehold-0.2.0/tests/test_process.py +56 -0
  258. leasehold-0.2.0/tests/test_quota.py +77 -0
  259. leasehold-0.2.0/tests/test_report.py +172 -0
  260. leasehold-0.2.0/tests/test_runner_hooks.py +147 -0
  261. leasehold-0.2.0/tests/test_session.py +137 -0
  262. leasehold-0.2.0/tests/test_setup.py +94 -0
  263. leasehold-0.2.0/tests/test_shims.py +203 -0
  264. leasehold-0.2.0/tests/test_standalone.py +62 -0
  265. leasehold-0.2.0/tests/test_status.py +189 -0
  266. leasehold-0.2.0/tests/test_top.py +378 -0
  267. leasehold-0.2.0/tests/test_user_mode.py +244 -0
  268. leasehold-0.2.0/tests/test_webui.py +164 -0
  269. leasehold-0.2.0/uv.lock +942 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "f=$(jq -r '.tool_input.file_path // empty'); case \"$f\" in *.py) ;; *) exit 0 ;; esac; cd \"$CLAUDE_PROJECT_DIR\" && uv run --no-sync ruff check --fix \"$f\" 1>&2 && uv run --no-sync ruff format \"$f\" 1>&2 || exit 2",
10
+ "statusMessage": "ruff"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "leasehold",
3
+ "owner": {
4
+ "name": "Matthew Strudwick"
5
+ },
6
+ "metadata": {
7
+ "description": "leasehold's Claude Code plugin: heavy commands take turns on a shared machine."
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "leasehold",
12
+ "source": "./integrations/claude-code",
13
+ "description": "Heavy Bash commands wait for a lease on a shared machine instead of piling on."
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,30 @@
1
+ name: Bug
2
+ description: Something does not work as the docs say.
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: What happened, and what you expected
9
+ validations:
10
+ required: true
11
+ - type: textarea
12
+ id: reproduce
13
+ attributes:
14
+ label: How to make it happen
15
+ description: Commands, the config (machine.toml) if it matters, and their output.
16
+ render: shell
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: doctor
21
+ attributes:
22
+ label: "`leasehold doctor` output"
23
+ render: text
24
+ - type: input
25
+ id: version
26
+ attributes:
27
+ label: "`leasehold --version`, distribution and kernel"
28
+ placeholder: leasehold 0.2.0, Debian 13, 6.12
29
+ validations:
30
+ required: true
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Security problem
4
+ url: https://github.com/chalkworth/leasehold/security/advisories/new
5
+ about: Report a vulnerability privately, not as an issue.
@@ -0,0 +1,15 @@
1
+ name: Idea
2
+ description: Something leasehold should do, or do better.
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: The problem
9
+ description: What you are trying to do on your machine, and what gets in the way today.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: idea
14
+ attributes:
15
+ label: What would help
@@ -0,0 +1,60 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ lint:
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 10
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: astral-sh/setup-uv@v10.3.0 # it publishes no moving major tag
19
+ - run: uv sync --python 3.12
20
+ - run: uv run --no-sync ruff check
21
+ - run: uv run --no-sync ruff format --check
22
+ - run: uv run --no-sync lint-imports
23
+
24
+ test:
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 20
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ python: ["3.12", "3.13", "3.14"]
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ - uses: astral-sh/setup-uv@v10.3.0 # it publishes no moving major tag
34
+ - run: uv sync --python ${{ matrix.python }}
35
+ - run: uv run --no-sync pytest -q
36
+
37
+ build:
38
+ runs-on: ubuntu-latest
39
+ timeout-minutes: 10
40
+ steps:
41
+ - uses: actions/checkout@v7
42
+ - uses: astral-sh/setup-uv@v10.3.0 # it publishes no moving major tag
43
+ - run: uv build
44
+ - name: The wheel carries its package data
45
+ run: |
46
+ python3 - <<'PY'
47
+ import glob, zipfile
48
+ names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
49
+ for needed in ("leasehold/static/ui.html", "leasehold/templates/machine.toml"):
50
+ assert needed in names, f"{needed} is not in the wheel"
51
+ PY
52
+ - name: It installs and runs from the wheel
53
+ run: |
54
+ uv venv /tmp/try && uv pip install --python /tmp/try/bin/python dist/*.whl
55
+ /tmp/try/bin/leasehold --version
56
+ /tmp/try/bin/leasehold init --print > /dev/null
57
+ - uses: actions/upload-artifact@v7
58
+ with:
59
+ name: dist
60
+ path: dist/
@@ -0,0 +1,37 @@
1
+ # Publishes docs/ (index.html and its media) to GitHub Pages. It runs only once the repository is
2
+ # public: a Pages site of a private repository can still be public, and nothing is published
3
+ # before the repository itself is. Pages must be set to "GitHub Actions" in the settings.
4
+ name: pages
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ paths: ["docs/**"]
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: pages
17
+ cancel-in-progress: true
18
+
19
+ jobs:
20
+ deploy:
21
+ if: github.event.repository.visibility == 'public'
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 10
24
+ permissions:
25
+ pages: write
26
+ id-token: write
27
+ environment:
28
+ name: github-pages
29
+ url: ${{ steps.deployment.outputs.page_url }}
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+ - uses: actions/configure-pages@v6
33
+ - uses: actions/upload-pages-artifact@v5
34
+ with:
35
+ path: docs
36
+ - id: deployment
37
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,42 @@
1
+ # Publishes to PyPI when a GitHub release is published, with PyPI's trusted publishing (no
2
+ # token is stored anywhere). It needs, once: a PyPI project with this repository and workflow
3
+ # named as a trusted publisher, and an environment "pypi" here. Nothing else triggers it.
4
+ name: release
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 10
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - uses: astral-sh/setup-uv@v10.3.0 # it publishes no moving major tag
20
+ - run: uv build
21
+ - name: The version is the release's
22
+ run: |
23
+ tag="${GITHUB_REF_NAME#v}"
24
+ ls dist/leasehold-"$tag"-py3-none-any.whl
25
+ - uses: actions/upload-artifact@v7
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ publish:
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 10
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write
37
+ steps:
38
+ - uses: actions/download-artifact@v8
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .import_linter_cache/
8
+ /bench-results/
@@ -0,0 +1,88 @@
1
+ # leasehold's import contracts, checked by `uv run lint-imports` (pre-commit and CI).
2
+ #
3
+ # The library (model, admission, ledger, quota) is pure policy and storage: it never imports
4
+ # the daemon. Commands that talk to a daemon (hold, handoff, report, top, the views and the MCP
5
+ # server) reach it only through `leasehold.client`, never by importing the agent, the API or the
6
+ # ledger.
7
+
8
+ [importlinter]
9
+ root_package = leasehold
10
+
11
+ [importlinter:contract:layers]
12
+ name = leasehold layers, top first: a module may import its own layer and those below
13
+ type = layers
14
+ layers =
15
+ cli
16
+ demo | mcp_server
17
+ setup
18
+ server | doctor | template | top | claude_code | shims
19
+ api
20
+ agent
21
+ status | lend | guard | events
22
+ machine
23
+ quota | observe | commands
24
+ shell
25
+ ledger
26
+ admission
27
+ model | discovery
28
+ paths
29
+ containers =
30
+ leasehold
31
+
32
+ [importlinter:contract:clients]
33
+ name = commands that talk to a daemon use only the client
34
+ type = forbidden
35
+ source_modules =
36
+ leasehold.busy
37
+ leasehold.hold
38
+ leasehold.handoff
39
+ leasehold.report
40
+ leasehold.webui
41
+ leasehold.top
42
+ leasehold.claude_code
43
+ leasehold.shims
44
+ leasehold.mcp_server
45
+ forbidden_modules =
46
+ leasehold.agent
47
+ leasehold.api
48
+ leasehold.server
49
+ leasehold.ledger
50
+ leasehold.machine
51
+ leasehold.admission
52
+
53
+ [importlinter:contract:leaves]
54
+ name = leaf modules import nothing else of leasehold's
55
+ type = forbidden
56
+ source_modules =
57
+ leasehold.model
58
+ leasehold.contain
59
+ leasehold.units
60
+ leasehold.process
61
+ leasehold.kinds
62
+ leasehold.exits
63
+ leasehold.client
64
+ leasehold.facts
65
+ leasehold.access
66
+ leasehold.paths
67
+ leasehold.jobs
68
+ leasehold.proctable
69
+ leasehold.metrics
70
+ leasehold.shell
71
+ leasehold.terminal
72
+ leasehold.hooknotes
73
+ forbidden_modules =
74
+ leasehold.admission
75
+ leasehold.ledger
76
+ leasehold.quota
77
+ leasehold.machine
78
+ leasehold.agent
79
+ leasehold.api
80
+ leasehold.server
81
+ leasehold.cli
82
+ leasehold.hold
83
+ leasehold.handoff
84
+ leasehold.report
85
+ leasehold.top
86
+ leasehold.status
87
+ leasehold.lend
88
+ leasehold.load
@@ -0,0 +1,15 @@
1
+ # `uv tool install pre-commit && pre-commit install` runs these on every commit.
2
+ repos:
3
+ - repo: https://github.com/astral-sh/ruff-pre-commit
4
+ rev: v0.17.0
5
+ hooks:
6
+ - id: ruff-check
7
+ - id: ruff-format
8
+ - repo: local
9
+ hooks:
10
+ - id: import-contracts
11
+ name: import contracts (.importlinter)
12
+ entry: uv run --no-sync lint-imports
13
+ language: system
14
+ pass_filenames: false
15
+ files: ^src/
@@ -0,0 +1,40 @@
1
+ # Working on leasehold
2
+
3
+ Notes for anyone changing this repository, people and coding agents alike. `CLAUDE.md` points
4
+ here.
5
+
6
+ ## Checks
7
+
8
+ ```sh
9
+ uv sync
10
+ uv run pytest -q # about a minute; daemon tests use real unix sockets under /tmp
11
+ uv run ruff check && uv run ruff format --check
12
+ uv run lint-imports # the layer contracts in .importlinter
13
+ ```
14
+
15
+ `pre-commit install` runs ruff and the import contracts on every commit.
16
+
17
+ ## Shape of the code
18
+
19
+ - The library is pure: `model` (pools), `admission` (the decision), `ledger` (SQLite),
20
+ `quota`. It never imports the daemon. `.importlinter` enforces the layers; fix an upward
21
+ import rather than listing it as an exception.
22
+ - The daemon is `machine` (config) → `agent` (requests, ticks) → `api` (routes, no sockets) →
23
+ `server` (the unix socket). `status` builds the status object as a pure function.
24
+ - `paths` says where each mode (system, user) keeps things; `discovery` is how a client finds
25
+ a daemon; `doctor` checks a setup; `template` renders the config `init` writes.
26
+ - `cli` is a package: one module per group of commands, each with a `register(sub)` and
27
+ handlers `(args, ctx) -> exit code`. A new command goes in the module of its group, or a new
28
+ module listed in `cli/__init__.py`.
29
+ - Anything that talks to a daemon (`hold`, `handoff`, `report`, the views) does so through
30
+ `leasehold.client` only.
31
+ - One module, one job. A new behaviour that does not belong to an existing module's docstring
32
+ gets its own module.
33
+
34
+ ## Conventions
35
+
36
+ - Comments and docstrings state what is guaranteed, once, in plain words. No task history.
37
+ - New behaviour comes with a test that would fail without it. Daemon behaviour is tested
38
+ against a real daemon on a temporary socket (`tests/support.py`).
39
+ - Every change to a public name, config key, CLI flag, exit code, status field or error code is
40
+ listed in `CHANGELOG.md` under the next release.
@@ -0,0 +1,81 @@
1
+ # Changelog
2
+
3
+ All notable changes to leasehold. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ and versions follow [Semantic Versioning](https://semver.org/) as described under "Stability" in
5
+ the README: before 1.0, a minor release may break the public API, and every break is listed
6
+ here.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - first public release
11
+
12
+ leasehold gives the heavy commands on one Linux machine (coding agents' builds and test runs, CI
13
+ jobs, people's work) a lease on a share of the machine before they start, sizes them to it, and,
14
+ where systemd allows it, holds them to it while they run.
15
+
16
+ ### Added
17
+
18
+ **For coding agents**
19
+
20
+ - `leasehold setup` (and `--remove`): a config sized for the machine, a systemd user unit
21
+ running your own daemon, the PATH shims, the agents found on PATH, and a containment and
22
+ `doctor` check, in one step.
23
+ - `leasehold run -- AGENT`: the agent as one session (`LEASEHOLD_SESSION`), the PATH shims first
24
+ on its PATH and, when contained, a slice of its own whose leftovers are stopped when it exits
25
+ (`--leave-running`, `--session`, `--quiet`, `--no-contain`).
26
+ - PATH shims (`leasehold shims install|list|remove`) for the programs the `[commands]` rules
27
+ name: a matching command runs under a lease, anything else passes straight through, and a
28
+ command an agent hook has leased already runs in that lease.
29
+ - The Claude Code plugin: a hook that takes a lease around a heavy Bash command, with a shell
30
+ pre-filter so ordinary commands start no Python, and a skill that tells the agent what a wait,
31
+ a refusal and the exit codes mean.
32
+ - `[commands]` rules (built in, extendable): which programs and arguments are heavy, their
33
+ weight and memory. A busy machine refuses after 20 s (`wait_s`, `when_busy = "deny"`), and the
34
+ refusal names what holds the pool and the place in line. `leasehold commands --test` and
35
+ `--programs`.
36
+ - `leasehold mcp` (experimental): MCP tools to read the machine's state and wait for a lease.
37
+
38
+ **Admission**
39
+
40
+ - Weighted leases on slots, renewed by their holder and expiring when it dies. Pools with
41
+ reserved and maximum slots and a priority; lease kinds `work`, `external`, `lend` and
42
+ `observed`. Turns go round by Unix user, then by session within a user; no backfill.
43
+ - Lending (experimental): when nothing waits and pressure stall information is quiet, `lend`
44
+ leases use spare slots, and are revoked the moment other work queues or pressure rises.
45
+ - The memory guard (`[guard]`): while memory pressure or available memory is past its
46
+ threshold, every pool admits only its reserved slots. On for a user's daemon.
47
+ - `[[observe]]` rules (experimental): a known program started outside leasehold counts as a
48
+ lease while it runs.
49
+
50
+ **Running a command**
51
+
52
+ - `leasehold hold -- CMD`: wait for a lease, run the command in its own process group (the
53
+ terminal's foreground when run from one), renew the lease, and release it when the command
54
+ ends. With no `--weight`, the command is weighed by the `[commands]` rules. The command learns
55
+ its size from `LEASEHOLD_JOBS` and the job-count variables cargo, make, CMake, pytest-xdist,
56
+ Go and others read; `--jobserver` adds a GNU make jobserver.
57
+ - Containment: where the caller runs in the user's own session and the systemd user manager is
58
+ delegated cpu and memory, `hold` and the shims start the command in a user scope with
59
+ `CPUWeight` 100 per slot and, with `--memory` or a rule's `memory`, `MemoryHigh`. `[contain]
60
+ mode` (`auto`, `off`), `hard_cpu`; `LEASEHOLD_CONTAIN=off`. Elsewhere leases are advisory.
61
+ - `hold --if-unavailable run|fail`: with no daemon, run the command without a lease (the
62
+ default) or exit 69.
63
+ - Exit codes: 69 no daemon, 75 busy (gave up waiting), 76 revoked, 77 refused, 127 not started.
64
+
65
+ **The daemon and views**
66
+
67
+ - `leasehold serve`: your own daemon (`--user`, no root) or the machine's (`--system`, a
68
+ `leasehold` group on its socket). One unix socket, a SQLite ledger, an HTTP API with a
69
+ versioned status object (schema 2).
70
+ - `leasehold init` writes a commented config sized for the machine: two pools for a user's
71
+ daemon, four for the machine's.
72
+ - `leasehold top` (terminal), `leasehold ui` (browser, experimental), `status`, `leases`,
73
+ `metrics` (Prometheus, experimental), `report` (a webhook, experimental) and `demo`, which
74
+ runs a made-up busy machine.
75
+ - `leasehold doctor`: the config, socket and its path length and permissions, the daemon, PSI,
76
+ cgroups, the ledger and containment, each with a fix.
77
+ - `bench/`: a seeded benchmark of several agents starting heavy jobs on one machine, run
78
+ unmanaged, under `nice`, in a low-weight systemd slice, with a fixed job count, and through
79
+ `leasehold hold` advisory and enforced (`bench/run.py`); method and results in `docs/bench.md`.
80
+ - The admission policy as a library (`leasehold.model`, `admission`, `ledger`, `quota`,
81
+ `client`), with no daemon.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,48 @@
1
+ # Contributing
2
+
3
+ Issues and pull requests are welcome. For anything larger than a fix, open an issue first, so
4
+ we can agree on the shape before you write it.
5
+
6
+ ## Setup
7
+
8
+ ```sh
9
+ git clone https://github.com/chalkworth/leasehold && cd leasehold
10
+ uv sync # Python 3.12+, the dev tools included
11
+ uv tool install pre-commit && pre-commit install # ruff and the import contracts on each commit
12
+ ```
13
+
14
+ ## Checks
15
+
16
+ Everything CI runs:
17
+
18
+ ```sh
19
+ uv run ruff check
20
+ uv run ruff format --check
21
+ uv run lint-imports # which module may import which (.importlinter)
22
+ uv run pytest -q # about two minutes; Linux only
23
+ ```
24
+
25
+ Tests start real daemons on real unix sockets in temporary directories. A few need tools:
26
+ `make` 4.4 for the jobserver test, which is skipped without it.
27
+
28
+ ## How the code is laid out
29
+
30
+ `AGENTS.md` describes the layers and the rules each module keeps. In short:
31
+
32
+ - The library (`model`, `admission`, `ledger`, `quota`) is pure policy and storage, and never
33
+ imports the daemon.
34
+ - Everything that talks to a daemon (`hold`, the views, the agent integrations) goes through
35
+ `leasehold.client`.
36
+ - The core depends on the standard library and pydantic only. Anything else is an optional
37
+ extra (`mcp`).
38
+
39
+ ## Changes that others depend on
40
+
41
+ The command line and its exit codes, the config file, the HTTP API and its `status` schema,
42
+ `--json` output and the library modules are public. A change to any of them gets a row in
43
+ `CHANGELOG.md`, saying what to do instead.
44
+
45
+ ## Licence
46
+
47
+ By contributing, you agree that your contribution is licensed under the Apache License 2.0,
48
+ the licence of this project.