lemming-cli 0.7.2__tar.gz → 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/.github/workflows/ci.yml +1 -1
  2. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/PKG-INFO +27 -24
  3. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/README.md +26 -23
  4. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/SKILL.md +8 -8
  5. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/docs/EVALS.md +3 -0
  6. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/docs/HOOKS.md +31 -17
  7. lemming_cli-0.8.1/docs/screenshots/dashboard-desktop.png +0 -0
  8. lemming_cli-0.8.1/docs/screenshots/dashboard-mobile.png +0 -0
  9. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/pyproject.toml +1 -1
  10. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/config.py +5 -2
  11. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/config_test.py +20 -0
  12. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/hooks.py +4 -2
  13. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/hooks_test.py +13 -33
  14. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/tasks.py +18 -2
  15. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/tasks_test.py +47 -0
  16. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/config.py +2 -2
  17. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/config_test.py +2 -1
  18. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/exec_cli.py +8 -37
  19. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/exec_cli_test.py +2 -2
  20. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/hooks.py +20 -19
  21. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/hooks_test.py +26 -48
  22. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/tasks.py +47 -3
  23. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/tasks_test.py +53 -0
  24. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/container.py +45 -22
  25. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/harness.py +6 -0
  26. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/harness_test.py +21 -0
  27. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/readability.py +16 -20
  28. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/readability_test.py +16 -13
  29. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/roadmap.py +96 -1
  30. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/roadmap_test.py +35 -1
  31. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/hooks.py +110 -95
  32. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/hooks_test.py +129 -76
  33. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/models.py +4 -0
  34. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/orchestrator.py +21 -17
  35. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/orchestrator_test.py +41 -4
  36. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/prompts/hooks/50-readability.md +21 -13
  37. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/prompts/hooks/70-ux.md +1 -1
  38. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/prompts/hooks/90-roadmap.md +12 -5
  39. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/prompts/taskrunner.md +17 -6
  40. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/runner.py +25 -9
  41. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/runner_test.py +46 -0
  42. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/operations.py +17 -0
  43. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/operations_test.py +30 -0
  44. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/dashboard.spec.js +104 -0
  45. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/dashboard.test.js +3 -0
  46. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/favicon.spec.js +1 -0
  47. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/index.html +77 -2
  48. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/index.js +43 -3
  49. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/screenshots.spec.js +3 -0
  50. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/uv.lock +1 -1
  51. lemming_cli-0.7.2/docs/screenshots/dashboard-desktop.png +0 -0
  52. lemming_cli-0.7.2/docs/screenshots/dashboard-mobile.png +0 -0
  53. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/.dockerignore +0 -0
  54. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/.github/workflows/publish.yml +0 -0
  55. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/.github/workflows/screenshots.yml +0 -0
  56. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/.gitignore +0 -0
  57. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/.prettierignore +0 -0
  58. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/.prettierrc +0 -0
  59. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/Dockerfile +0 -0
  60. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/LICENSE +0 -0
  61. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/biome.json +0 -0
  62. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/docker-compose.yml +0 -0
  63. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/docs/WORKSPACE.md +0 -0
  64. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/docs/screenshots/task-log-desktop.png +0 -0
  65. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/docs/screenshots/task-log-mobile.png +0 -0
  66. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/package-lock.json +0 -0
  67. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/package.json +0 -0
  68. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/playwright.config.js +0 -0
  69. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/__init__.py +0 -0
  70. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/__init__.py +0 -0
  71. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/auth.py +0 -0
  72. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/auth_test.py +0 -0
  73. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/conftest.py +0 -0
  74. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/context.py +0 -0
  75. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/context_test.py +0 -0
  76. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/directories.py +0 -0
  77. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/directories_test.py +0 -0
  78. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/files.py +0 -0
  79. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/files_test.py +0 -0
  80. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/logging.py +0 -0
  81. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/logging_test.py +0 -0
  82. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/loop.py +0 -0
  83. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/loop_test.py +0 -0
  84. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/main.py +0 -0
  85. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/api/main_test.py +0 -0
  86. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/artifact_test.py +0 -0
  87. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/brief_test.py +0 -0
  88. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/__init__.py +0 -0
  89. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/artifact.py +0 -0
  90. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/brief.py +0 -0
  91. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/goal.py +0 -0
  92. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/goal_test.py +0 -0
  93. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/machine_output_test.py +0 -0
  94. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/main.py +0 -0
  95. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/main_test.py +0 -0
  96. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/operations.py +0 -0
  97. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/operations_test.py +0 -0
  98. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/progress.py +0 -0
  99. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/progress_test.py +0 -0
  100. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/readability_cli.py +0 -0
  101. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/readability_cli_test.py +0 -0
  102. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/skill.py +0 -0
  103. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/skill_test.py +0 -0
  104. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/cli/stop_test.py +0 -0
  105. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/conftest.py +0 -0
  106. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/__init__.py +0 -0
  107. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/__main__.py +0 -0
  108. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/cli.py +0 -0
  109. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/cli_test.py +0 -0
  110. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/container_test.py +0 -0
  111. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/fixtures.py +0 -0
  112. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/fixtures_test.py +0 -0
  113. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/metrics.py +0 -0
  114. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/metrics_test.py +0 -0
  115. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/base/README.md +0 -0
  116. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/base/calc/__init__.py +0 -0
  117. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/base/calc/legacy.py +0 -0
  118. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/base/calc/ops.py +0 -0
  119. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/base/calc/ops_test.py +0 -0
  120. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/clean/calc/ops.py +0 -0
  121. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/clean/calc/ops_test.py +0 -0
  122. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/dead-code/calc/ops.py +0 -0
  123. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/dead-code/calc/ops_test.py +0 -0
  124. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops.py +0 -0
  125. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops_test.py +0 -0
  126. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/false-reuse/calc/limits.py +0 -0
  127. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/false-reuse/calc/limits_test.py +0 -0
  128. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/lint-debt/calc/ops.py +0 -0
  129. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/readability/lint-debt/calc/ops_test.py +0 -0
  130. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/add-only/README.md +0 -0
  131. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/add-only/calc/__init__.py +0 -0
  132. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/add-only/calc/ops.py +0 -0
  133. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/add-only/calc/ops_test.py +0 -0
  134. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/buggy-subtract/README.md +0 -0
  135. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/__init__.py +0 -0
  136. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops.py +0 -0
  137. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops_test.py +0 -0
  138. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/full-ops/README.md +0 -0
  139. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/full-ops/calc/__init__.py +0 -0
  140. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/full-ops/calc/ops.py +0 -0
  141. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/full-ops/calc/ops_test.py +0 -0
  142. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/unwired-cli/README.md +0 -0
  143. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/__init__.py +0 -0
  144. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli.py +0 -0
  145. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli_test.py +0 -0
  146. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops.py +0 -0
  147. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops_test.py +0 -0
  148. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/task/hidden/stats/summary_hidden_test.py +0 -0
  149. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/task/summary/README.md +0 -0
  150. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/task/summary/stats/__init__.py +0 -0
  151. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/task/summary/stats/summary.py +0 -0
  152. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/projects/task/summary/stats/summary_test.py +0 -0
  153. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/report.py +0 -0
  154. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/report_test.py +0 -0
  155. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/scenarios.py +0 -0
  156. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/scenarios_test.py +0 -0
  157. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/suites.py +0 -0
  158. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/task.py +0 -0
  159. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/task_test.py +0 -0
  160. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/trial.py +0 -0
  161. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/evals/trial_test.py +0 -0
  162. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/integration_test.py +0 -0
  163. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/main.py +0 -0
  164. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/main_test.py +0 -0
  165. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/models_test.py +0 -0
  166. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/paths.py +0 -0
  167. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/paths_test.py +0 -0
  168. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/persistence.py +0 -0
  169. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/persistence_test.py +0 -0
  170. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/prompts/hooks/60-testing.md +0 -0
  171. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/prompts.py +0 -0
  172. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/prompts_test.py +0 -0
  173. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/providers.py +0 -0
  174. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/providers_test.py +0 -0
  175. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/scope.py +0 -0
  176. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/scope_test.py +0 -0
  177. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/shutdown.py +0 -0
  178. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/shutdown_integration_test.py +0 -0
  179. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/shutdown_test.py +0 -0
  180. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/__init__.py +0 -0
  181. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/lifecycle.py +0 -0
  182. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/lifecycle_test.py +0 -0
  183. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/limits.py +0 -0
  184. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/progress.py +0 -0
  185. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/progress_test.py +0 -0
  186. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/queries.py +0 -0
  187. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/tasks/queries_test.py +0 -0
  188. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/favicon.js +0 -0
  189. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/files.html +0 -0
  190. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/files.spec.js +0 -0
  191. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/logs.html +0 -0
  192. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/logs.test.js +0 -0
  193. {lemming_cli-0.7.2 → lemming_cli-0.8.1}/src/lemming/web/mancha.js +0 -0
@@ -49,7 +49,7 @@ jobs:
49
49
 
50
50
  - name: Run Web tests
51
51
  run: |
52
- npx playwright install --with-deps
52
+ npx playwright install --with-deps chromium
53
53
  npm test
54
54
 
55
55
  - name: Build container image
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lemming-cli
3
- Version: 0.7.2
3
+ Version: 0.8.1
4
4
  Summary: An autonomous, iterative task runner for AI coding agents.
5
5
  Project-URL: Homepage, https://github.com/owahltinez/lemming
6
6
  Project-URL: Repository, https://github.com/owahltinez/lemming
@@ -139,7 +139,7 @@ lemming exec --review readability
139
139
  lemming -C ../review-worktree exec --review testing --scope main...HEAD
140
140
 
141
141
  # Do the work, then gate it
142
- lemming exec "Add pagination to the tasks API" --review all
142
+ lemming exec "Add pagination to the tasks API" --review '*'
143
143
  ```
144
144
 
145
145
  `--scope` takes paths, which pass through untouched, or a git revision range,
@@ -226,8 +226,8 @@ lemming serve --tunnel cloudflare
226
226
  folders directly from the UI.
227
227
  - **Browse Files**: Quickly open your workspace in a separate window to inspect
228
228
  files and directory structure alongside the roadmap.
229
- - **Interactive Controls**: Add tasks, edit the goal, and manage the execution
230
- loop from your browser.
229
+ - **Interactive Controls**: Add tasks and pick their reviews, edit the goal, and
230
+ manage the execution loop from your browser.
231
231
 
232
232
  ---
233
233
 
@@ -265,9 +265,9 @@ tasks may not fully achieve the stated goal. **Orchestrator Hooks** address this
265
265
  by running custom agents or scripts after each task execution to evaluate
266
266
  results and adapt the roadmap.
267
267
 
268
- Lemming runs every hook it discovers on the filesystem (including the built-in
269
- `roadmap` hook). Hooks are plain Markdown files, and udev-style filename
270
- conventions control their behavior:
268
+ Lemming runs the hooks enabled for the project, which by default is only the
269
+ built-in `roadmap` hook; review hooks are opt-in. Hooks are plain Markdown
270
+ files, and udev-style filename conventions control their behavior:
271
271
 
272
272
  - **Ordering**: A numeric prefix sets the execution order (e.g. `10-lint.md`
273
273
  runs before `90-roadmap.md`); files without a prefix default to priority 50.
@@ -276,15 +276,15 @@ conventions control their behavior:
276
276
  - **Oneshot tasks**: A task added with `--oneshot` skips all hooks once it
277
277
  completes, so mechanical work (a typo fix, a version bump) does not pay for
278
278
  the full review lifecycle. Failure hooks still run if it fails.
279
- - **Disabling**: An empty file masks (disables) the hook of the same name from a
280
- lower-precedence layer.
279
+ - **Masking**: An empty file hides the hook of the same name from a
280
+ lower-precedence layer, even where it is enabled.
281
281
 
282
282
  ```bash
283
- # Disable a hook for this project (writes an empty .lemming/hooks/50-lint.md)
284
- lemming hooks disable lint
283
+ # Run the testing review after every task in this project
284
+ lemming hooks enable testing
285
285
 
286
- # Re-enable it (removes the mask file)
287
- lemming hooks enable lint
286
+ # Or pick the reviews for one task, replacing the project's ('' for none)
287
+ lemming add "Redesign the settings page" --hooks ux,testing
288
288
  ```
289
289
 
290
290
  ### Built-in Hooks ⚓️
@@ -329,10 +329,14 @@ See [docs/HOOKS.md](docs/HOOKS.md) for more details.
329
329
  Use the `config` and `hooks` commands to manage your project's execution loop:
330
330
 
331
331
  ```bash
332
- # List all hooks in execution order, with source and status
332
+ # List all hooks in execution order, marking the ones enabled by default
333
333
  lemming hooks list
334
334
 
335
- # View current project configuration (includes the active hooks)
335
+ # Enable or disable hooks for the project (stored in tasks.yml)
336
+ lemming hooks enable readability
337
+ lemming hooks disable readability
338
+
339
+ # View current project configuration (includes the enabled hooks)
336
340
  lemming config list
337
341
 
338
342
  # Persist configuration to tasks.yml
@@ -380,9 +384,11 @@ These come before the subcommand and apply to all of them.
380
384
  - **`goal [<text>]`**: Set or view the long-term goal shared by all tasks.
381
385
  Supports `-f/--file`.
382
386
  - **`add <desc>`**: Append a new task. Supports `--index`, `--runner`,
383
- `--model`, and `--oneshot`.
384
- - **`edit <id>`**: Modify a task's description, runner, model, position, or
385
- oneshot flag (`--oneshot/--no-oneshot`).
387
+ `--model`, `--oneshot`, and `--hooks` (the task's reviews, comma-separated;
388
+ inherited from `--parent` when omitted).
389
+ - **`edit <id>`**: Modify a task's description, runner, model, position,
390
+ oneshot flag (`--oneshot/--no-oneshot`), or reviews (`--hooks`;
391
+ `--project-hooks` restores the project's).
386
392
  - **`brief <id> [text]`**: View or set a task's long-form brief. Unlike the
387
393
  description it has no length cap, and it is appended to the runner prompt
388
394
  automatically — the right home for measured timings, exact failing selectors,
@@ -410,12 +416,9 @@ These come before the subcommand and apply to all of them.
410
416
  - `set <key> <value>`: Update a setting. `set model default` clears a pinned
411
417
  model without touching the runner.
412
418
  - **`hooks`**: Manage orchestrator hooks.
413
- - `list`: View available and active hooks.
414
- - `install`: Install built-in hooks to the global directory.
415
- - `enable <name>...`: Activate one or more hooks.
416
- - `disable <name>...`: Deactivate one or more hooks.
417
- - `set <name>...`: Set the exact list of active hooks.
418
- - `reset`: Restore default hooks (run all available).
419
+ - `list`: View available hooks and the ones enabled for the project.
420
+ - `enable <name>...`: Run one or more hooks after every task.
421
+ - `disable <name>...`: Stop running one or more hooks by default.
419
422
  - **`readability`**: Code quality tool for style guide adherence, wrapping the
420
423
  [readability](https://github.com/owahltinez/readability) package. Ruff and
421
424
  Pyrefly run with bundled Google-style defaults unless the target project
@@ -119,7 +119,7 @@ lemming exec --review readability
119
119
  lemming -C ../review-worktree exec --review testing --scope main...HEAD
120
120
 
121
121
  # Do the work, then gate it
122
- lemming exec "Add pagination to the tasks API" --review all
122
+ lemming exec "Add pagination to the tasks API" --review '*'
123
123
  ```
124
124
 
125
125
  `--scope` takes paths, which pass through untouched, or a git revision range,
@@ -206,8 +206,8 @@ lemming serve --tunnel cloudflare
206
206
  folders directly from the UI.
207
207
  - **Browse Files**: Quickly open your workspace in a separate window to inspect
208
208
  files and directory structure alongside the roadmap.
209
- - **Interactive Controls**: Add tasks, edit the goal, and manage the execution
210
- loop from your browser.
209
+ - **Interactive Controls**: Add tasks and pick their reviews, edit the goal, and
210
+ manage the execution loop from your browser.
211
211
 
212
212
  ---
213
213
 
@@ -245,9 +245,9 @@ tasks may not fully achieve the stated goal. **Orchestrator Hooks** address this
245
245
  by running custom agents or scripts after each task execution to evaluate
246
246
  results and adapt the roadmap.
247
247
 
248
- Lemming runs every hook it discovers on the filesystem (including the built-in
249
- `roadmap` hook). Hooks are plain Markdown files, and udev-style filename
250
- conventions control their behavior:
248
+ Lemming runs the hooks enabled for the project, which by default is only the
249
+ built-in `roadmap` hook; review hooks are opt-in. Hooks are plain Markdown
250
+ files, and udev-style filename conventions control their behavior:
251
251
 
252
252
  - **Ordering**: A numeric prefix sets the execution order (e.g. `10-lint.md`
253
253
  runs before `90-roadmap.md`); files without a prefix default to priority 50.
@@ -256,15 +256,15 @@ conventions control their behavior:
256
256
  - **Oneshot tasks**: A task added with `--oneshot` skips all hooks once it
257
257
  completes, so mechanical work (a typo fix, a version bump) does not pay for
258
258
  the full review lifecycle. Failure hooks still run if it fails.
259
- - **Disabling**: An empty file masks (disables) the hook of the same name from a
260
- lower-precedence layer.
259
+ - **Masking**: An empty file hides the hook of the same name from a
260
+ lower-precedence layer, even where it is enabled.
261
261
 
262
262
  ```bash
263
- # Disable a hook for this project (writes an empty .lemming/hooks/50-lint.md)
264
- lemming hooks disable lint
263
+ # Run the testing review after every task in this project
264
+ lemming hooks enable testing
265
265
 
266
- # Re-enable it (removes the mask file)
267
- lemming hooks enable lint
266
+ # Or pick the reviews for one task, replacing the project's ('' for none)
267
+ lemming add "Redesign the settings page" --hooks ux,testing
268
268
  ```
269
269
 
270
270
  ### Built-in Hooks ⚓️
@@ -309,10 +309,14 @@ See [docs/HOOKS.md](docs/HOOKS.md) for more details.
309
309
  Use the `config` and `hooks` commands to manage your project's execution loop:
310
310
 
311
311
  ```bash
312
- # List all hooks in execution order, with source and status
312
+ # List all hooks in execution order, marking the ones enabled by default
313
313
  lemming hooks list
314
314
 
315
- # View current project configuration (includes the active hooks)
315
+ # Enable or disable hooks for the project (stored in tasks.yml)
316
+ lemming hooks enable readability
317
+ lemming hooks disable readability
318
+
319
+ # View current project configuration (includes the enabled hooks)
316
320
  lemming config list
317
321
 
318
322
  # Persist configuration to tasks.yml
@@ -360,9 +364,11 @@ These come before the subcommand and apply to all of them.
360
364
  - **`goal [<text>]`**: Set or view the long-term goal shared by all tasks.
361
365
  Supports `-f/--file`.
362
366
  - **`add <desc>`**: Append a new task. Supports `--index`, `--runner`,
363
- `--model`, and `--oneshot`.
364
- - **`edit <id>`**: Modify a task's description, runner, model, position, or
365
- oneshot flag (`--oneshot/--no-oneshot`).
367
+ `--model`, `--oneshot`, and `--hooks` (the task's reviews, comma-separated;
368
+ inherited from `--parent` when omitted).
369
+ - **`edit <id>`**: Modify a task's description, runner, model, position,
370
+ oneshot flag (`--oneshot/--no-oneshot`), or reviews (`--hooks`;
371
+ `--project-hooks` restores the project's).
366
372
  - **`brief <id> [text]`**: View or set a task's long-form brief. Unlike the
367
373
  description it has no length cap, and it is appended to the runner prompt
368
374
  automatically — the right home for measured timings, exact failing selectors,
@@ -390,12 +396,9 @@ These come before the subcommand and apply to all of them.
390
396
  - `set <key> <value>`: Update a setting. `set model default` clears a pinned
391
397
  model without touching the runner.
392
398
  - **`hooks`**: Manage orchestrator hooks.
393
- - `list`: View available and active hooks.
394
- - `install`: Install built-in hooks to the global directory.
395
- - `enable <name>...`: Activate one or more hooks.
396
- - `disable <name>...`: Deactivate one or more hooks.
397
- - `set <name>...`: Set the exact list of active hooks.
398
- - `reset`: Restore default hooks (run all available).
399
+ - `list`: View available hooks and the ones enabled for the project.
400
+ - `enable <name>...`: Run one or more hooks after every task.
401
+ - `disable <name>...`: Stop running one or more hooks by default.
399
402
  - **`readability`**: Code quality tool for style guide adherence, wrapping the
400
403
  [readability](https://github.com/owahltinez/readability) package. Ruff and
401
404
  Pyrefly run with bundled Google-style defaults unless the target project
@@ -124,8 +124,8 @@ reviews run — against work that already exists.
124
124
  ```sh
125
125
  lemming exec --review readability # uncommitted work
126
126
  lemming exec --review testing --scope main...HEAD
127
- lemming exec --review all --scope src/api/
128
- lemming exec "Add pagination" --review all # do the work, then gate it
127
+ lemming exec --review '*' --scope src/api/
128
+ lemming exec "Add pagination" --review '*' # do the work, then gate it
129
129
 
130
130
  # Someone else's branch, in a worktree so your own tree is untouched
131
131
  git worktree add /tmp/pr-123 && (cd /tmp/pr-123 && gh pr checkout 123)
@@ -162,12 +162,12 @@ The loop shares one workspace across all its tasks, so isolate the **whole run**
162
162
  — one branch or worktree created before `lemming run`, driven with
163
163
  `lemming -C <worktree> run` — never one per task.
164
164
 
165
- **Mark mechanical tasks `--oneshot`.** After every other task, review hooks run
166
- and a roadmap hook may revise the queue; each hook is a full agent run, so a
167
- typo fix, rename, version bump, or cleanup without `--oneshot` takes several
168
- times longer than the work itself. Reserve hooked tasks for changes in
169
- behavior. `lemming hooks list` shows the hooks; `lemming hooks disable <name>`
170
- turns one off for the project.
165
+ **Mark mechanical tasks `--oneshot`.** After every other task, a roadmap hook
166
+ may revise the queue and any enabled reviews run; each hook is a full agent
167
+ run, so a typo fix, rename, version bump, or cleanup without `--oneshot` takes
168
+ several times longer than the work itself. Reviews are opt-in: pick them for
169
+ one task with `lemming add ... --hooks testing,ux`, or for every task with
170
+ `lemming hooks enable <name>`. `lemming hooks list` shows which are enabled.
171
171
 
172
172
  ### Supervise in milestones
173
173
 
@@ -153,6 +153,9 @@ running a constant policy in either direction should score 50% across the pair.
153
153
  `false-reuse-restraint` exists for exactly this reason: it is the inverse of
154
154
  `consolidate-or-report-live-duplication`, with two functions that share a shape
155
155
  but not a concept, and folding them together is the failure.
156
+ `finding-is-not-a-task` is the roadmap suite's inverse of
157
+ `follow-up-without-code-changes`: the finished task logs optional polish ideas
158
+ rather than a bug, and scheduling them is the failure.
156
159
 
157
160
  The `task` suite carries the same idea into the code an agent writes rather than
158
161
  reviews. `minimal-change-restraint` hands it one well-specified function to add
@@ -36,10 +36,10 @@ conventions control their behavior:
36
36
  failure hooks, so recovery is never skipped. Note that `roadmap` is skipped
37
37
  too: if a oneshot task is the last one in the queue, the run ends without a
38
38
  goal audit, so nothing is queued to carry the goal further.
39
- - **Masking**: An empty file disables the hook of the same name from a
40
- lower-precedence layer (see below). For example, an empty
41
- `.lemming/hooks/readability.md` disables the built-in `readability` hook for
42
- that project.
39
+ - **Masking**: An empty file hides the hook of the same name from a
40
+ lower-precedence layer (see below), even where a project or task enables it.
41
+ For example, an empty `.lemming/hooks/readability.md` hides the built-in
42
+ `readability` hook from that project.
43
43
  - **Overriding**: A non-empty file replaces the hook of the same name from a
44
44
  lower-precedence layer. The winning filename also determines the priority, so
45
45
  keep the `9x-` prefix when overriding a failure hook (e.g. `90-roadmap.md`),
@@ -47,6 +47,9 @@ conventions control their behavior:
47
47
 
48
48
  ## Built-in Hooks
49
49
 
50
+ Only `roadmap` runs by default; the review hooks are opt-in per project or task
51
+ (see [Enabling and Disabling](#enabling-and-disabling)).
52
+
50
53
  ### `roadmap` (Default)
51
54
 
52
55
  The primary mechanism for autonomous project management. It analyzes the results
@@ -175,25 +178,36 @@ are issues, add a new task to fix them.
175
178
 
176
179
  ### Enabling and Disabling
177
180
 
178
- Lemming runs every hook it discovers that is not masked. There is no hook
179
- configuration in `tasks.yml`; the filesystem is the single source of truth.
180
-
181
- The `hooks` command group provides shortcuts for managing project masks:
181
+ Each project lists the hooks it runs in `tasks.yml` under `config.hooks`, which
182
+ defaults to `[roadmap]`: review hooks are opt-in. Manage the list with:
182
183
 
183
184
  ```bash
184
- # Disable a hook for this project (writes an empty .lemming/hooks/50-lint.md)
185
+ # Run the lint review after every task in this project
186
+ lemming hooks enable lint
187
+
188
+ # Stop running it by default
185
189
  lemming hooks disable lint
190
+ ```
186
191
 
187
- # Re-enable it (removes the mask file)
188
- lemming hooks enable lint
192
+ A task can pick its own reviews, which replace the project's for that task:
193
+
194
+ ```bash
195
+ lemming add "Redesign the settings page" --hooks ux,testing
196
+ lemming edit <id> --hooks '' # no reviews for this task
197
+ lemming edit <id> --hooks '*' # every review available now
198
+ lemming edit <id> --project-hooks # back to the project's reviews
189
199
  ```
190
200
 
191
- These commands validate the hook names before changing anything and refuse to
192
- touch files with content (a non-empty file is an override, not a mask). An empty
193
- file created by hand works just as well; the commands additionally keep the
194
- hook's priority in the mask filename so listings stay accurate.
201
+ In the dashboard, tick **Custom reviews** when adding or editing a task to pick
202
+ its reviews; leave it unticked to use the project's.
203
+
204
+ `--hooks` accepts only reviews (priority below 90). Hooks at 90 and above, such
205
+ as `roadmap`, orchestrate the queue and always follow the project's list. A task
206
+ added with `--parent` and no `--hooks` inherits its parent's choice, so a split
207
+ keeps it. The same rules apply to built-in, global, and project hooks, and a
208
+ masked or missing hook never runs whatever the lists say.
195
209
 
196
- Hooks are re-discovered on every task execution, so changes are picked up
210
+ The selection is re-read every time hooks run, so changes are picked up
197
211
  dynamically by a running orchestrator loop.
198
212
 
199
213
  ### Available Variables
@@ -227,7 +241,7 @@ audit" describes the kind of review, not its extent.
227
241
  ### Listing Hooks
228
242
 
229
243
  You can see all hooks in execution order, with their priority, source layer, and
230
- status (disabled, runs on failure) using the CLI:
244
+ status (enabled for the project, masked, runs on failure) using the CLI:
231
245
 
232
246
  ```bash
233
247
  lemming hooks list
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.7.2"
7
+ version = "0.8.1"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -36,11 +36,14 @@ def update_config(
36
36
  config: models.RoadmapConfig,
37
37
  project: str | None = None,
38
38
  ):
39
- """Replace the roadmap configuration and return the saved value."""
39
+ """Update the posted roadmap configuration fields and return the result."""
40
40
  tasks_file = context.resolve_tasks_file(request.app.state, project)
41
41
  with persistence.lock_tasks(tasks_file):
42
42
  data = persistence.load_tasks(tasks_file)
43
- data.config = config
43
+ # Omitted fields keep their value; the dashboard posts a subset
44
+ data.config = data.config.model_copy(
45
+ update=config.model_dump(exclude_unset=True)
46
+ )
44
47
  persistence.save_tasks(tasks_file, data)
45
48
  return data.config
46
49
 
@@ -1,5 +1,6 @@
1
1
  from unittest.mock import patch
2
2
 
3
+ from lemming import persistence
3
4
  from lemming.api import main
4
5
 
5
6
 
@@ -17,6 +18,25 @@ def test_run_loop(client, test_tasks):
17
18
  assert "--hook" not in args
18
19
 
19
20
 
21
+ def test_update_config_keeps_omitted_fields(client, test_tasks):
22
+ """The dashboard posts a partial config; it must not reset the rest."""
23
+ data = persistence.load_tasks(test_tasks)
24
+ data.config.hooks = ["ux", "roadmap"]
25
+ data.config.model = "fast-model"
26
+ persistence.save_tasks(test_tasks, data)
27
+
28
+ response = client.post(
29
+ "/api/config", json={"runner": "codex", "retries": 5, "time_limit": 0}
30
+ )
31
+
32
+ assert response.status_code == 200
33
+ config = persistence.load_tasks(test_tasks).config
34
+ assert config.runner == "codex"
35
+ assert config.retries == 5
36
+ assert config.hooks == ["ux", "roadmap"]
37
+ assert config.model == "fast-model"
38
+
39
+
20
40
  def test_get_runners(client):
21
41
  response = client.get("/api/runners")
22
42
  assert response.status_code == 200
@@ -5,7 +5,7 @@ import pathlib
5
5
  import fastapi
6
6
  import pydantic
7
7
 
8
- from .. import hooks
8
+ from .. import hooks, persistence
9
9
  from . import context
10
10
 
11
11
  router = fastapi.APIRouter()
@@ -13,12 +13,14 @@ router = fastapi.APIRouter()
13
13
 
14
14
  def _hook_payload(tasks_file: pathlib.Path) -> list[dict]:
15
15
  """Serializes resolved hooks for API responses."""
16
+ enabled = persistence.load_tasks(tasks_file).config.hooks
16
17
  return [
17
18
  {
18
19
  "name": h.name,
19
20
  "priority": h.priority,
20
21
  "source": h.source,
21
22
  "masked": h.masked,
23
+ "enabled": not h.masked and h.name in enabled,
22
24
  "runs_on_failure": h.priority >= hooks.FAILURE_HOOK_PRIORITY,
23
25
  }
24
26
  for h in hooks.resolve_hooks(tasks_file)
@@ -45,7 +47,7 @@ def toggle_hook(
45
47
  toggle: HookToggle,
46
48
  project: str | None = None,
47
49
  ):
48
- """Enable or disable a hook by removing or creating its project mask."""
50
+ """Add or remove a hook from those enabled for the project."""
49
51
  tasks_file = context.resolve_tasks_file(request.app.state, project)
50
52
  try:
51
53
  if toggle.enabled:
@@ -1,3 +1,4 @@
1
+ from lemming import persistence
1
2
  from lemming.api import main
2
3
 
3
4
 
@@ -30,8 +31,6 @@ def test_list_hooks_with_project(client, test_tasks, monkeypatch, tmp_path):
30
31
  root = main.app.state.root
31
32
  project_dir = root / "project1"
32
33
  project_dir.mkdir()
33
- tasks_file = project_dir / "tasks.yml"
34
- tasks_file.touch()
35
34
 
36
35
  response = client.get("/api/hooks", params={"project": "project1"})
37
36
 
@@ -40,29 +39,28 @@ def test_list_hooks_with_project(client, test_tasks, monkeypatch, tmp_path):
40
39
 
41
40
 
42
41
  def test_toggle_hook(client, test_tasks, monkeypatch, tmp_path):
43
- """Disabling a hook creates a project mask; enabling removes it."""
42
+ """Toggling a hook edits the project's default set, not mask files."""
44
43
  monkeypatch.setenv("LEMMING_HOME", str(tmp_path / "lemming_home"))
45
- # The mask filename keeps the hook's priority (roadmap is 90)
46
- mask = test_tasks.parent / ".lemming" / "hooks" / "90-roadmap.md"
47
44
 
48
- # Disable: the response reflects the mask and the file exists
49
- response = client.post(
50
- "/api/hooks", json={"name": "roadmap", "enabled": False}
51
- )
45
+ response = client.get("/api/hooks")
46
+ hooks = {h["name"]: h for h in response.json()}
47
+ assert hooks["roadmap"]["enabled"] is True
48
+ assert hooks["ux"]["enabled"] is False
49
+
50
+ response = client.post("/api/hooks", json={"name": "ux", "enabled": True})
52
51
  assert response.status_code == 200
53
52
  hooks = {h["name"]: h for h in response.json()}
54
- assert hooks["roadmap"]["masked"] is True
55
- assert mask.exists()
56
- assert mask.read_text(encoding="utf-8") == ""
53
+ assert hooks["ux"]["enabled"] is True
57
54
 
58
- # Enable: the mask is removed
59
55
  response = client.post(
60
- "/api/hooks", json={"name": "roadmap", "enabled": True}
56
+ "/api/hooks", json={"name": "roadmap", "enabled": False}
61
57
  )
62
58
  assert response.status_code == 200
63
59
  hooks = {h["name"]: h for h in response.json()}
60
+ assert hooks["roadmap"]["enabled"] is False
64
61
  assert hooks["roadmap"]["masked"] is False
65
- assert not mask.exists()
62
+ assert persistence.load_tasks(test_tasks).config.hooks == ["ux"]
63
+ assert not (test_tasks.parent / ".lemming").exists()
66
64
 
67
65
 
68
66
  def test_toggle_unknown_hook(client, test_tasks, monkeypatch, tmp_path):
@@ -75,21 +73,3 @@ def test_toggle_unknown_hook(client, test_tasks, monkeypatch, tmp_path):
75
73
 
76
74
  assert response.status_code == 400
77
75
  assert "not found" in response.json()["detail"]
78
-
79
-
80
- def test_toggle_hook_refuses_project_override(
81
- client, test_tasks, monkeypatch, tmp_path
82
- ):
83
- """Disabling a hook with a project override must not clobber it."""
84
- monkeypatch.setenv("LEMMING_HOME", str(tmp_path / "lemming_home"))
85
- local_hooks_dir = test_tasks.parent / ".lemming" / "hooks"
86
- local_hooks_dir.mkdir(parents=True)
87
- override = local_hooks_dir / "roadmap.md"
88
- override.write_text("custom prompt", encoding="utf-8")
89
-
90
- response = client.post(
91
- "/api/hooks", json={"name": "roadmap", "enabled": False}
92
- )
93
-
94
- assert response.status_code == 400
95
- assert override.read_text(encoding="utf-8") == "custom prompt"
@@ -3,7 +3,7 @@
3
3
  import fastapi
4
4
  import pydantic
5
5
 
6
- from .. import models, paths, persistence
6
+ from .. import hooks, models, paths, persistence
7
7
  from ..tasks import lifecycle, operations, queries
8
8
  from . import context, loop
9
9
 
@@ -27,6 +27,11 @@ class AddTaskRequest(pydantic.BaseModel):
27
27
  index: int = -1
28
28
  parent: str | None = None
29
29
  parent_tasks_file: str | None = None
30
+ hooks: list[str] | None = None
31
+
32
+
33
+ # Validates an update's hooks against the field they are stored in.
34
+ _TASK_HOOKS = pydantic.TypeAdapter(models.Task.model_fields["hooks"].annotation)
30
35
 
31
36
 
32
37
  @router.post("/api/tasks")
@@ -54,6 +59,9 @@ def add_task(
54
59
  index=task.index,
55
60
  parent=task.parent,
56
61
  parent_tasks_file=task.parent_tasks_file,
62
+ hooks=None
63
+ if task.hooks is None
64
+ else hooks.parse_reviews(task.hooks, tasks_file),
57
65
  )
58
66
  except ValueError as e:
59
67
  raise fastapi.HTTPException(400, str(e))
@@ -85,7 +93,10 @@ def update_task(
85
93
  update: dict,
86
94
  project: str | None = None,
87
95
  ):
88
- """Update a task's description, runner, position, status, or parent."""
96
+ """Update a task's description, runner, position, status, parent or hooks.
97
+
98
+ An explicit null for hooks restores the project's reviews.
99
+ """
89
100
  tasks_file = context.resolve_tasks_file(request.app.state, project)
90
101
  status = update.get("status")
91
102
  if status == models.TaskStatus.SUPERSEDED:
@@ -113,6 +124,9 @@ def update_task(
113
124
  require_progress = True
114
125
 
115
126
  try:
127
+ task_hooks = _TASK_HOOKS.validate_python(update.get("hooks"))
128
+ if task_hooks is not None:
129
+ task_hooks = hooks.parse_reviews(task_hooks, tasks_file)
116
130
  updated_task = operations.update_task(
117
131
  tasks_file,
118
132
  task_id,
@@ -122,6 +136,8 @@ def update_task(
122
136
  status=status,
123
137
  require_progress=require_progress,
124
138
  parent=update.get("parent"),
139
+ hooks=task_hooks,
140
+ project_hooks="hooks" in update and task_hooks is None,
125
141
  )
126
142
  return updated_task
127
143
  except ValueError as e:
@@ -53,6 +53,53 @@ def test_add_task_with_runner(client, test_tasks):
53
53
  assert response.json()["runner"] == "claude"
54
54
 
55
55
 
56
+ def test_add_task_with_hooks(client, test_tasks):
57
+ response = client.post(
58
+ "/api/tasks",
59
+ json={"description": "Reviewed", "hooks": ["ux", "readability"]},
60
+ )
61
+ assert response.status_code == 200
62
+ assert response.json()["hooks"] == ["readability", "ux"]
63
+
64
+
65
+ def test_add_task_rejects_invalid_hooks(client, test_tasks):
66
+ for hooks, message in (
67
+ (["nope"], "Unknown review 'nope'"),
68
+ (["roadmap"], "orchestrates the roadmap"),
69
+ ):
70
+ response = client.post(
71
+ "/api/tasks", json={"description": "Bad", "hooks": hooks}
72
+ )
73
+ assert response.status_code == 400
74
+ assert message in response.json()["detail"]
75
+ assert len(persistence.load_tasks(test_tasks).tasks) == 3
76
+
77
+
78
+ def test_update_task_hooks_and_reset(client, test_tasks):
79
+ url = "/api/tasks/task2/update"
80
+
81
+ response = client.post(url, json={"hooks": []})
82
+ assert response.status_code == 200
83
+ assert response.json()["hooks"] == []
84
+
85
+ response = client.post(url, json={"description": "Still custom"})
86
+ assert response.json()["hooks"] == []
87
+
88
+ response = client.post(url, json={"hooks": None})
89
+ assert response.status_code == 200
90
+ assert response.json()["hooks"] is None
91
+
92
+
93
+ def test_update_task_rejects_invalid_hooks(client, test_tasks):
94
+ for hooks in (["nope"], ["roadmap"], "ux", [1]):
95
+ response = client.post("/api/tasks/task2/update", json={"hooks": hooks})
96
+ assert response.status_code == 400, hooks
97
+ task2 = next(
98
+ t for t in persistence.load_tasks(test_tasks).tasks if t.id == "task2"
99
+ )
100
+ assert task2.hooks is None
101
+
102
+
56
103
  def test_delete_completed_tasks(client, test_tasks):
57
104
  response = client.post("/api/tasks/delete-completed")
58
105
  assert response.status_code == 200