lemming-cli 0.6.1__tar.gz → 0.7.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 (191) hide show
  1. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/PKG-INFO +9 -1
  2. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/README.md +8 -0
  3. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/SKILL.md +62 -10
  4. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/pyproject.toml +1 -1
  5. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/operations.py +31 -0
  6. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/operations_test.py +47 -0
  7. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/tasks.py +5 -1
  8. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/orchestrator.py +72 -1
  9. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/orchestrator_test.py +145 -0
  10. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/90-roadmap.md +9 -3
  11. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/taskrunner.md +4 -0
  12. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/queries.py +27 -0
  13. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/queries_test.py +36 -0
  14. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/uv.lock +1 -1
  15. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.dockerignore +0 -0
  16. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.github/workflows/ci.yml +0 -0
  17. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.github/workflows/publish.yml +0 -0
  18. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.github/workflows/screenshots.yml +0 -0
  19. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.gitignore +0 -0
  20. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.prettierignore +0 -0
  21. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.prettierrc +0 -0
  22. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/Dockerfile +0 -0
  23. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/LICENSE +0 -0
  24. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/biome.json +0 -0
  25. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docker-compose.yml +0 -0
  26. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/EVALS.md +0 -0
  27. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/HOOKS.md +0 -0
  28. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/WORKSPACE.md +0 -0
  29. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/dashboard-desktop.png +0 -0
  30. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/dashboard-mobile.png +0 -0
  31. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/task-log-desktop.png +0 -0
  32. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/task-log-mobile.png +0 -0
  33. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/package-lock.json +0 -0
  34. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/package.json +0 -0
  35. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/playwright.config.js +0 -0
  36. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/__init__.py +0 -0
  37. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/__init__.py +0 -0
  38. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/auth.py +0 -0
  39. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/auth_test.py +0 -0
  40. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/config.py +0 -0
  41. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/config_test.py +0 -0
  42. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/conftest.py +0 -0
  43. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/context.py +0 -0
  44. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/context_test.py +0 -0
  45. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/directories.py +0 -0
  46. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/directories_test.py +0 -0
  47. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/files.py +0 -0
  48. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/files_test.py +0 -0
  49. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/hooks.py +0 -0
  50. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/hooks_test.py +0 -0
  51. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/logging.py +0 -0
  52. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/logging_test.py +0 -0
  53. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/loop.py +0 -0
  54. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/loop_test.py +0 -0
  55. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/main.py +0 -0
  56. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/main_test.py +0 -0
  57. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/tasks.py +0 -0
  58. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/tasks_test.py +0 -0
  59. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/artifact_test.py +0 -0
  60. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/brief_test.py +0 -0
  61. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/__init__.py +0 -0
  62. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/artifact.py +0 -0
  63. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/brief.py +0 -0
  64. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/config.py +0 -0
  65. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/config_test.py +0 -0
  66. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/exec_cli.py +0 -0
  67. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/exec_cli_test.py +0 -0
  68. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/goal.py +0 -0
  69. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/goal_test.py +0 -0
  70. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/hooks.py +0 -0
  71. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/hooks_test.py +0 -0
  72. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/machine_output_test.py +0 -0
  73. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/main.py +0 -0
  74. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/main_test.py +0 -0
  75. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/progress.py +0 -0
  76. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/progress_test.py +0 -0
  77. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/readability_cli.py +0 -0
  78. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/readability_cli_test.py +0 -0
  79. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/skill.py +0 -0
  80. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/skill_test.py +0 -0
  81. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/stop_test.py +0 -0
  82. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/tasks_test.py +0 -0
  83. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/conftest.py +0 -0
  84. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/__init__.py +0 -0
  85. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/__main__.py +0 -0
  86. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/cli.py +0 -0
  87. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/cli_test.py +0 -0
  88. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/container.py +0 -0
  89. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/container_test.py +0 -0
  90. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/fixtures.py +0 -0
  91. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/fixtures_test.py +0 -0
  92. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/harness.py +0 -0
  93. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/harness_test.py +0 -0
  94. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/metrics.py +0 -0
  95. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/metrics_test.py +0 -0
  96. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/README.md +0 -0
  97. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/__init__.py +0 -0
  98. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/legacy.py +0 -0
  99. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/ops.py +0 -0
  100. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/ops_test.py +0 -0
  101. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/clean/calc/ops.py +0 -0
  102. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/clean/calc/ops_test.py +0 -0
  103. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/dead-code/calc/ops.py +0 -0
  104. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/dead-code/calc/ops_test.py +0 -0
  105. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops.py +0 -0
  106. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops_test.py +0 -0
  107. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/false-reuse/calc/limits.py +0 -0
  108. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/false-reuse/calc/limits_test.py +0 -0
  109. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/lint-debt/calc/ops.py +0 -0
  110. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/lint-debt/calc/ops_test.py +0 -0
  111. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/README.md +0 -0
  112. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/calc/__init__.py +0 -0
  113. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/calc/ops.py +0 -0
  114. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/calc/ops_test.py +0 -0
  115. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/README.md +0 -0
  116. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/__init__.py +0 -0
  117. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops.py +0 -0
  118. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops_test.py +0 -0
  119. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/README.md +0 -0
  120. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/calc/__init__.py +0 -0
  121. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/calc/ops.py +0 -0
  122. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/calc/ops_test.py +0 -0
  123. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/README.md +0 -0
  124. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/__init__.py +0 -0
  125. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli.py +0 -0
  126. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli_test.py +0 -0
  127. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops.py +0 -0
  128. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops_test.py +0 -0
  129. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/hidden/stats/summary_hidden_test.py +0 -0
  130. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/README.md +0 -0
  131. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/stats/__init__.py +0 -0
  132. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/stats/summary.py +0 -0
  133. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/stats/summary_test.py +0 -0
  134. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/readability.py +0 -0
  135. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/readability_test.py +0 -0
  136. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/report.py +0 -0
  137. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/report_test.py +0 -0
  138. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/roadmap.py +0 -0
  139. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/roadmap_test.py +0 -0
  140. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/scenarios.py +0 -0
  141. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/scenarios_test.py +0 -0
  142. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/suites.py +0 -0
  143. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/task.py +0 -0
  144. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/task_test.py +0 -0
  145. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/trial.py +0 -0
  146. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/trial_test.py +0 -0
  147. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/hooks.py +0 -0
  148. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/hooks_test.py +0 -0
  149. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/integration_test.py +0 -0
  150. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/main.py +0 -0
  151. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/main_test.py +0 -0
  152. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/models.py +0 -0
  153. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/models_test.py +0 -0
  154. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/paths.py +0 -0
  155. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/paths_test.py +0 -0
  156. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/persistence.py +0 -0
  157. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/persistence_test.py +0 -0
  158. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/50-readability.md +0 -0
  159. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/60-testing.md +0 -0
  160. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/70-ux.md +0 -0
  161. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts.py +0 -0
  162. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts_test.py +0 -0
  163. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/providers.py +0 -0
  164. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/providers_test.py +0 -0
  165. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/runner.py +0 -0
  166. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/runner_test.py +0 -0
  167. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/scope.py +0 -0
  168. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/scope_test.py +0 -0
  169. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/shutdown.py +0 -0
  170. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/shutdown_integration_test.py +0 -0
  171. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/shutdown_test.py +0 -0
  172. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/__init__.py +0 -0
  173. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/lifecycle.py +0 -0
  174. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/lifecycle_test.py +0 -0
  175. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/limits.py +0 -0
  176. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/operations.py +0 -0
  177. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/operations_test.py +0 -0
  178. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/progress.py +0 -0
  179. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/progress_test.py +0 -0
  180. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/dashboard.spec.js +0 -0
  181. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/dashboard.test.js +0 -0
  182. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/favicon.js +0 -0
  183. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/favicon.spec.js +0 -0
  184. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/files.html +0 -0
  185. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/files.spec.js +0 -0
  186. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/index.html +0 -0
  187. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/index.js +0 -0
  188. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/logs.html +0 -0
  189. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/logs.test.js +0 -0
  190. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/mancha.js +0 -0
  191. {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/screenshots.spec.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lemming-cli
3
- Version: 0.6.1
3
+ Version: 0.7.0
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
@@ -445,6 +445,14 @@ These come before the subcommand and apply to all of them.
445
445
  - `--yolo`: Run the runner in auto-approve mode (default: True).
446
446
  - `--env`: Set environment variables for the runner (e.g., `--env KEY=VALUE`).
447
447
  - `--no-defaults`: Skip default flag injection for known runners.
448
+ - `--until <task-id>`: Stop between tasks once that task has completed or
449
+ failed, printing `Reached milestone <id>; M pending.` so a supervising
450
+ agent can review before queueing the next milestone. Tasks inserted ahead
451
+ of it still run; if the roadmap hook supersedes it, the run continues until
452
+ every replacement has settled. Deleting it stops the run with exit 1.
453
+ - `--max-tasks N`: Stop between tasks once `N` have completed or failed,
454
+ printing `Stopped after N tasks; M pending.` Attempts that leave a task
455
+ pending do not count. Combine with `--until` as a budget cap.
448
456
  - `--`: Use `--` to pass any flag directly to the underlying runner. A
449
457
  per-task `--runner`/`--model` overrides anything passed here.
450
458
  - **`exec [<description>]`**: Run one task, or one set of reviews, outside any
@@ -425,6 +425,14 @@ These come before the subcommand and apply to all of them.
425
425
  - `--yolo`: Run the runner in auto-approve mode (default: True).
426
426
  - `--env`: Set environment variables for the runner (e.g., `--env KEY=VALUE`).
427
427
  - `--no-defaults`: Skip default flag injection for known runners.
428
+ - `--until <task-id>`: Stop between tasks once that task has completed or
429
+ failed, printing `Reached milestone <id>; M pending.` so a supervising
430
+ agent can review before queueing the next milestone. Tasks inserted ahead
431
+ of it still run; if the roadmap hook supersedes it, the run continues until
432
+ every replacement has settled. Deleting it stops the run with exit 1.
433
+ - `--max-tasks N`: Stop between tasks once `N` have completed or failed,
434
+ printing `Stopped after N tasks; M pending.` Attempts that leave a task
435
+ pending do not count. Combine with `--until` as a budget cap.
428
436
  - `--`: Use `--` to pass any flag directly to the underlying runner. A
429
437
  per-task `--runner`/`--model` overrides anything passed here.
430
438
  - **`exec [<description>]`**: Run one task, or one set of reviews, outside any
@@ -153,33 +153,85 @@ recorded progress, so the project survives context limits.
153
153
  lemming goal "Add offline support with a service worker and a sync queue"
154
154
  lemming add "Register the service worker and cache the app shell"
155
155
  lemming add "Queue writes in IndexedDB while offline"
156
+ lemming add "Bump the service worker cache version" --oneshot
156
157
  lemming run # runs until the queue drains
158
+ lemming run --until <task-id> # or stops once that task settles
157
159
  ```
158
160
 
159
161
  The loop shares one workspace across all its tasks, so isolate the **whole run**
160
162
  — one branch or worktree created before `lemming run`, driven with
161
163
  `lemming -C <worktree> run` — never one per task.
162
164
 
163
- Check on it, and read what an agent actually did:
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.
171
+
172
+ ### Supervise in milestones
173
+
174
+ Never run the whole roadmap and poll it from one context. Every `status` call
175
+ lands in your context window, and the run outlives it. Split the queue into
176
+ milestones instead:
177
+
178
+ 1. Queue the whole roadmap, pick the task that ends the first milestone, and
179
+ run `lemming run --until <task-id>`. It blocks, runs anything the roadmap
180
+ hook inserts ahead of that task, follows the task through any split, and
181
+ exits printing `Reached milestone <id>; M pending.` or
182
+ `All tasks completed!`. A non-zero exit means a task failed, the queue is
183
+ blocked, or the milestone task was deleted; `lemming status --brief` shows
184
+ which. Add `--max-tasks N` as a budget cap when a runaway roadmap hook
185
+ would be expensive.
186
+ 2. Between milestones, judge direction: `lemming status --brief`, then
187
+ `lemming status <id>` for the tasks that matter. Re-plan with `add`, `edit`,
188
+ `supersede`, or `delete`, then run the next milestone.
189
+ 3. Delegate each milestone to a subagent when one exists, so the polling and
190
+ the log reading stay out of your context. The subagent reports back in the
191
+ fixed shape below, nothing more.
192
+
193
+ This is how a stronger model directs cheaper ones: `lemming config set runner`
194
+ and `config set model` pick the worker; the supervising agent only reads
195
+ status between milestones and decides what happens next.
196
+
197
+ Checking on a running milestone is fine when a task might be stuck, but poll
198
+ sparingly and read the smallest thing that answers the question:
164
199
 
165
200
  ```sh
166
- lemming status # queue, attempts, and recorded progress
167
- lemming status <id> # one task in detail, including its runner command
168
- lemming logs # the active task's log, or logs <id> for one task
201
+ lemming status --brief # one row per task, no descriptions
202
+ lemming status <id> # Run Time against the time limit, progress entries
203
+ lemming logs <id> | tail -n 30 # never the whole log
169
204
  ```
170
205
 
171
- Adjust it while it runs — new tasks are picked up automatically:
206
+ A task is stuck when its run time approaches the limit with no new progress
207
+ entries and the log tail shows the same step repeating. Otherwise leave it.
208
+
209
+ Adjust the queue while it runs — new tasks are picked up automatically:
172
210
 
173
211
  ```sh
174
212
  lemming add "Add a retry backoff to the sync queue" --index 0 # jump the queue
175
213
  lemming stop --after-current-task # drain, then stop
176
214
  ```
177
215
 
178
- After each task, review hooks run automatically and a roadmap hook may revise
179
- the queue. `lemming hooks list` shows them; `lemming hooks disable <name>` turns
180
- one off for the project. Queue a mechanical task — a typo fix, a version bump, a
181
- rename — with `lemming add "..." --oneshot` to skip those hooks when it
182
- completes.
216
+ ### Report tersely
217
+
218
+ Your own words fill context faster than any tool. Whether reporting to a user or
219
+ to a parent agent:
220
+
221
+ - Report one milestone in this shape and no other: outcome (`done`, `failed`,
222
+ `blocked`), task IDs that settled, files changed, one line of guidance for the
223
+ next milestone. Skip anything that can be re-read from `lemming status`.
224
+ - Never paste `status`, `logs`, test output, or diffs into a message or a
225
+ progress entry; name the task ID or file and let the reader fetch it.
226
+ - Progress entries are one line each. Evidence goes into
227
+ `lemming artifact <id> <name> --file -`, not the entry.
228
+
229
+ ### Do not nest
230
+
231
+ Never call `lemming run` or `lemming exec` from inside a task runner or a hook.
232
+ Each level spawns another full agent against the same quota, and the inner loop
233
+ competes with the outer one for the same tasks file. A runner that needs more
234
+ work done adds tasks with `lemming add`; the running loop picks them up.
183
235
 
184
236
  ## What to know before running any of it
185
237
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.6.1"
7
+ version = "0.7.0"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -62,6 +62,23 @@ def quiet_poll_log_config() -> dict:
62
62
  is_flag=True,
63
63
  help="Do not auto-inject default flags (like --yolo) based on runner name.",
64
64
  )
65
+ @click.option(
66
+ "--max-tasks",
67
+ type=click.IntRange(min=1),
68
+ help=(
69
+ "Stop between tasks once this many have completed or failed, so a "
70
+ "caller can drive the roadmap one milestone at a time."
71
+ ),
72
+ )
73
+ @click.option(
74
+ "--until",
75
+ "until_id",
76
+ metavar="TASK_ID",
77
+ help=(
78
+ "Stop between tasks once this task, or every task that replaced it, "
79
+ "has completed or failed. Accepts an unambiguous ID prefix."
80
+ ),
81
+ )
65
82
  @click.argument("runner_args", nargs=-1, type=click.UNPROCESSED)
66
83
  @click.pass_context
67
84
  def run(
@@ -70,12 +87,24 @@ def run(
70
87
  yolo: bool,
71
88
  env: tuple,
72
89
  no_defaults: bool,
90
+ max_tasks: int | None,
91
+ until_id: str | None,
73
92
  runner_args: tuple,
74
93
  ) -> None:
75
94
  """Starts the orchestrator loop to autonomously execute pending tasks."""
76
95
  tasks_file = ctx.obj["TASKS_FILE"]
77
96
  verbose = ctx.obj["VERBOSE"]
78
97
 
98
+ # Resolve the milestone up front so a typo fails before any task runs.
99
+ until = None
100
+ if until_id:
101
+ try:
102
+ data = persistence.load_tasks(tasks_file)
103
+ until = queries.resolve_task(data.tasks, until_id).id
104
+ except (models.TaskNotFoundError, models.AmbiguousTaskIdError) as e:
105
+ click.echo(f"Error: {e}")
106
+ ctx.exit(1)
107
+
79
108
  # Determine the project's working directory
80
109
  working_dir = paths.get_working_dir(tasks_file)
81
110
 
@@ -111,6 +140,8 @@ def run(
111
140
  no_defaults,
112
141
  runner_args,
113
142
  working_dir=working_dir,
143
+ max_tasks=max_tasks,
144
+ until=until,
114
145
  )
115
146
  finally:
116
147
  persistence.release_loop_lock(tasks_file)
@@ -86,6 +86,53 @@ class TestCLIOperations(unittest.TestCase):
86
86
  self.assertEqual(result.exit_code, 1)
87
87
  self.assertIn("Another loop is already running", result.output)
88
88
 
89
+ def test_run_forwards_max_tasks(self):
90
+ with mock.patch.object(
91
+ operations, "run_loop", return_value=True
92
+ ) as run_loop:
93
+ result = self.cli_runner.invoke(
94
+ cli.cli, self.base_args + ["run", "--max-tasks", "2"]
95
+ )
96
+
97
+ self.assertEqual(result.exit_code, 0)
98
+ self.assertEqual(run_loop.call_args.kwargs["max_tasks"], 2)
99
+
100
+ def test_run_resolves_until_prefix(self):
101
+ persistence.save_tasks(
102
+ self.test_tasks_file,
103
+ models.Roadmap(
104
+ tasks=[models.Task(id="abcd1234", description="Milestone")]
105
+ ),
106
+ )
107
+
108
+ with mock.patch.object(
109
+ operations, "run_loop", return_value=True
110
+ ) as run_loop:
111
+ result = self.cli_runner.invoke(
112
+ cli.cli, self.base_args + ["run", "--until", "abcd"]
113
+ )
114
+
115
+ self.assertEqual(result.exit_code, 0)
116
+ self.assertEqual(run_loop.call_args.kwargs["until"], "abcd1234")
117
+
118
+ def test_run_rejects_unknown_until(self):
119
+ with mock.patch.object(operations, "run_loop") as run_loop:
120
+ result = self.cli_runner.invoke(
121
+ cli.cli, self.base_args + ["run", "--until", "nope"]
122
+ )
123
+
124
+ self.assertEqual(result.exit_code, 1)
125
+ self.assertIn("Task nope not found", result.output)
126
+ run_loop.assert_not_called()
127
+
128
+ def test_run_rejects_zero_max_tasks(self):
129
+ result = self.cli_runner.invoke(
130
+ cli.cli, self.base_args + ["run", "--max-tasks", "0"]
131
+ )
132
+
133
+ self.assertNotEqual(result.exit_code, 0)
134
+ self.assertIn("not in the range", result.output)
135
+
89
136
  def test_serve_help(self):
90
137
  result = self.cli_runner.invoke(cli.cli, ["serve", "--help"])
91
138
  self.assertEqual(result.exit_code, 0)
@@ -106,7 +106,11 @@ def _echo_task_summary(
106
106
  @click.option(
107
107
  "--oneshot",
108
108
  is_flag=True,
109
- help="Skip post-task orchestrator hooks when this task completes.",
109
+ help=(
110
+ "Skip post-task orchestrator hooks when this task completes. Use for "
111
+ "mechanical work (renames, typo fixes, version bumps, cleanups); each "
112
+ "hook is a full agent run."
113
+ ),
110
114
  )
111
115
  @click.pass_context
112
116
  def add(
@@ -516,6 +516,54 @@ def _handle_runner_exit(
516
516
  return False
517
517
 
518
518
 
519
+ _SETTLED_STATUSES = frozenset(
520
+ {
521
+ models.TaskStatus.COMPLETED,
522
+ models.TaskStatus.FAILED,
523
+ models.TaskStatus.CANCELLED,
524
+ models.TaskStatus.SUPERSEDED,
525
+ }
526
+ )
527
+
528
+
529
+ def _task_settled(tasks_file: pathlib.Path, task_id: str) -> bool:
530
+ """Reports whether a task has reached a terminal status."""
531
+ data = persistence.load_tasks(tasks_file)
532
+ task = next((t for t in data.tasks if t.id == task_id), None)
533
+ return task is not None and task.status in _SETTLED_STATUSES
534
+
535
+
536
+ def _count_pending(data: models.Roadmap) -> int:
537
+ return sum(t.status == models.TaskStatus.PENDING for t in data.tasks)
538
+
539
+
540
+ def _report_task_limit(data: models.Roadmap, settled: int) -> None:
541
+ """Prints the one line a caller needs after a bounded run stops."""
542
+ task_label = "task" if settled == 1 else "tasks"
543
+ click.echo(
544
+ f"Stopped after {settled} {task_label}; {_count_pending(data)} pending."
545
+ )
546
+
547
+
548
+ def _check_milestone(data: models.Roadmap, until: str) -> bool | None:
549
+ """Decides whether a milestone run should stop, reporting why.
550
+
551
+ Returns:
552
+ True to stop successfully, False to stop because the milestone task
553
+ vanished, None to keep running.
554
+ """
555
+ reached = queries.milestone_reached(data.tasks, until)
556
+ if reached is None:
557
+ click.echo(f"Milestone task {until} no longer exists; stopping.")
558
+ return False
559
+ if reached:
560
+ click.echo(
561
+ f"Reached milestone {until}; {_count_pending(data)} pending."
562
+ )
563
+ return True
564
+ return None
565
+
566
+
519
567
  def run_loop(
520
568
  tasks_file: pathlib.Path,
521
569
  verbose: bool,
@@ -527,6 +575,8 @@ def run_loop(
527
575
  hooks: list[str] | None = None,
528
576
  scope: str | None = None,
529
577
  once: bool = False,
578
+ max_tasks: int | None = None,
579
+ until: str | None = None,
530
580
  ) -> bool:
531
581
  """Runs pending tasks, returning True only when the roadmap is complete.
532
582
 
@@ -546,14 +596,22 @@ def run_loop(
546
596
  once: Run only the first task, through its configured attempt budget.
547
597
  Exhaustion leaves it pending, and failed finalization stops rather
548
598
  than turning a review placeholder back into runnable work.
599
+ max_tasks: Stop between tasks once this many have reached a terminal
600
+ status, so a caller can run the roadmap one milestone at a time.
601
+ Attempts that leave a task pending do not count.
602
+ until: Exact ID of a milestone task. Stop between tasks once it, or
603
+ every task that replaced it, has settled; stop unsuccessfully if
604
+ it is deleted.
549
605
 
550
606
  Returns:
551
- True only when the roadmap ran to completion.
607
+ True when the roadmap ran to completion, the task limit was reached,
608
+ or the milestone was reached.
552
609
  """
553
610
  one_task_id = None
554
611
  one_task_config = None
555
612
  one_task_runner = None
556
613
  one_task_model = None
614
+ settled_count = 0
557
615
  while True:
558
616
  returncode = 0
559
617
 
@@ -567,6 +625,16 @@ def run_loop(
567
625
  # (e.g., from Web UI)
568
626
  data = persistence.load_tasks(tasks_file)
569
627
 
628
+ # Bounded runs stop between tasks, after hooks have settled the last
629
+ # one, so the caller sees a consistent roadmap.
630
+ if max_tasks is not None and settled_count >= max_tasks:
631
+ _report_task_limit(data, settled_count)
632
+ return True
633
+ if until is not None:
634
+ milestone = _check_milestone(data, until)
635
+ if milestone is not None:
636
+ return milestone
637
+
570
638
  retries = data.config.retries
571
639
  time_limit = data.config.time_limit
572
640
  runner_name = data.config.runner
@@ -663,6 +731,7 @@ def run_loop(
663
731
  )
664
732
  if should_abort:
665
733
  return False
734
+ settled_count += _task_settled(tasks_file, task_id)
666
735
  continue
667
736
 
668
737
  # Add a small random jitter to avoid race conditions between
@@ -717,6 +786,7 @@ def run_loop(
717
786
  (t for t in finalized.tasks if t.id == task_id), None
718
787
  )
719
788
  return bool(task and task.status == models.TaskStatus.COMPLETED)
789
+ settled_count += _task_settled(tasks_file, task_id)
720
790
  continue
721
791
 
722
792
  prompt = prompts.prepare_prompt(
@@ -822,6 +892,7 @@ def run_loop(
822
892
  )
823
893
  if should_abort:
824
894
  return False
895
+ settled_count += _task_settled(tasks_file, task_id)
825
896
 
826
897
  if once:
827
898
  updated = persistence.load_tasks(tasks_file)
@@ -1053,6 +1053,151 @@ def test_run_hooks_reloads_tasks(mock_run, mock_prepare, hooks_env):
1053
1053
  assert mock_load.call_count == 5
1054
1054
 
1055
1055
 
1056
+ def _complete_current_task(tasks_file):
1057
+ """Builds a runner stub that marks whichever task it runs as completed."""
1058
+
1059
+ def side_effect(cmd, tasks_file_arg, task_id, *args, **kwargs):
1060
+ with persistence.lock_tasks(tasks_file):
1061
+ data = persistence.load_tasks(tasks_file)
1062
+ task = next(t for t in data.tasks if t.id == task_id)
1063
+ task.status = models.TaskStatus.COMPLETED
1064
+ task.completed_at = time.time()
1065
+ persistence.save_tasks(tasks_file, data)
1066
+ return (0, "output", "")
1067
+
1068
+ return side_effect
1069
+
1070
+
1071
+ @mock.patch("lemming.runner.run_with_heartbeat")
1072
+ def test_run_loop_max_tasks_stops_between_tasks(mock_run, setup_env, capsys):
1073
+ """A milestone limit exits cleanly and leaves the queue intact."""
1074
+ test_tasks_file, _ = setup_env
1075
+ operations.add_task(test_tasks_file, "Task 2")
1076
+ operations.add_task(test_tasks_file, "Task 3")
1077
+ mock_run.side_effect = _complete_current_task(test_tasks_file)
1078
+
1079
+ completed = run_loop(
1080
+ test_tasks_file,
1081
+ verbose=False,
1082
+ retry_delay=0,
1083
+ yolo=True,
1084
+ no_defaults=False,
1085
+ runner_args=(),
1086
+ hooks=[],
1087
+ max_tasks=2,
1088
+ )
1089
+
1090
+ assert completed is True
1091
+ statuses = [t.status for t in persistence.load_tasks(test_tasks_file).tasks]
1092
+ assert statuses == [
1093
+ models.TaskStatus.COMPLETED,
1094
+ models.TaskStatus.COMPLETED,
1095
+ models.TaskStatus.PENDING,
1096
+ ]
1097
+ assert "Stopped after 2 tasks; 1 pending" in capsys.readouterr().out
1098
+
1099
+
1100
+ @mock.patch("lemming.runner.run_with_heartbeat")
1101
+ def test_run_loop_max_tasks_counts_only_settled_tasks(
1102
+ mock_run, setup_env, capsys
1103
+ ):
1104
+ """An attempt that leaves the task pending does not consume the budget."""
1105
+ test_tasks_file, _ = setup_env
1106
+ operations.add_task(test_tasks_file, "Task 2")
1107
+ attempts = []
1108
+
1109
+ def flaky_then_complete(cmd, tasks_file_arg, task_id, *args, **kwargs):
1110
+ attempts.append(task_id)
1111
+ if len(attempts) == 1:
1112
+ return (0, "no completion reported", "")
1113
+ return _complete_current_task(test_tasks_file)(
1114
+ cmd, tasks_file_arg, task_id, *args, **kwargs
1115
+ )
1116
+
1117
+ mock_run.side_effect = flaky_then_complete
1118
+
1119
+ completed = run_loop(
1120
+ test_tasks_file,
1121
+ verbose=False,
1122
+ retry_delay=0,
1123
+ yolo=True,
1124
+ no_defaults=False,
1125
+ runner_args=(),
1126
+ hooks=[],
1127
+ max_tasks=1,
1128
+ )
1129
+
1130
+ assert completed is True
1131
+ assert attempts == ["task1", "task1"]
1132
+ statuses = [t.status for t in persistence.load_tasks(test_tasks_file).tasks]
1133
+ assert statuses == [models.TaskStatus.COMPLETED, models.TaskStatus.PENDING]
1134
+
1135
+
1136
+ @mock.patch("lemming.runner.run_with_heartbeat")
1137
+ def test_run_loop_until_stops_after_milestone_task(mock_run, setup_env, capsys):
1138
+ """The loop runs inserted tasks and stops once the target settles."""
1139
+ test_tasks_file, _ = setup_env
1140
+ milestone = operations.add_task(test_tasks_file, "Milestone")
1141
+ operations.add_task(test_tasks_file, "After milestone")
1142
+ mock_run.side_effect = _complete_current_task(test_tasks_file)
1143
+
1144
+ completed = run_loop(
1145
+ test_tasks_file,
1146
+ verbose=False,
1147
+ retry_delay=0,
1148
+ yolo=True,
1149
+ no_defaults=False,
1150
+ runner_args=(),
1151
+ hooks=[],
1152
+ until=milestone.id,
1153
+ )
1154
+
1155
+ assert completed is True
1156
+ statuses = [t.status for t in persistence.load_tasks(test_tasks_file).tasks]
1157
+ assert statuses == [
1158
+ models.TaskStatus.COMPLETED,
1159
+ models.TaskStatus.COMPLETED,
1160
+ models.TaskStatus.PENDING,
1161
+ ]
1162
+ assert f"Reached milestone {milestone.id}; 1 pending" in (
1163
+ capsys.readouterr().out
1164
+ )
1165
+
1166
+
1167
+ @mock.patch("lemming.runner.run_with_heartbeat")
1168
+ def test_run_loop_until_stops_when_milestone_task_vanishes(
1169
+ mock_run, setup_env, capsys
1170
+ ):
1171
+ """Deleting the target mid-run is reported instead of running the queue."""
1172
+ test_tasks_file, _ = setup_env
1173
+ milestone = operations.add_task(test_tasks_file, "Milestone")
1174
+
1175
+ def complete_and_delete(cmd, tasks_file_arg, task_id, *args, **kwargs):
1176
+ operations.delete_tasks(test_tasks_file, milestone.id)
1177
+ return _complete_current_task(test_tasks_file)(
1178
+ cmd, tasks_file_arg, task_id, *args, **kwargs
1179
+ )
1180
+
1181
+ mock_run.side_effect = complete_and_delete
1182
+
1183
+ completed = run_loop(
1184
+ test_tasks_file,
1185
+ verbose=False,
1186
+ retry_delay=0,
1187
+ yolo=True,
1188
+ no_defaults=False,
1189
+ runner_args=(),
1190
+ hooks=[],
1191
+ until=milestone.id,
1192
+ )
1193
+
1194
+ assert completed is False
1195
+ assert mock_run.call_count == 1
1196
+ assert f"Milestone task {milestone.id} no longer exists" in (
1197
+ capsys.readouterr().out
1198
+ )
1199
+
1200
+
1056
1201
  @mock.patch("lemming.runner.run_with_heartbeat")
1057
1202
  def test_run_loop_records_resolved_command(mock_run, setup_env):
1058
1203
  """Provenance is persisted so the model behind a commit is recoverable."""
@@ -62,6 +62,12 @@ the plan up-to-date with minimal friction.
62
62
  Include the concrete files, symbols, motivation, and acceptance criteria the
63
63
  task needs, but do not repeat project-wide rules already present in the
64
64
  long-term goal.
65
+ **Mechanical tasks MUST be added with `--oneshot`**: cleanups, renames,
66
+ typo fixes, version bumps, deleting scratch files, applying formatter
67
+ output, and the follow-ups from directive 7. Every hook that runs after
68
+ such a task costs a full agent run, often longer than the task itself.
69
+ Reserve hooked tasks for work that changes behavior. Mark existing
70
+ mechanical tasks with `edit <id> --oneshot` when you notice them.
65
71
  9. **No Code Changes**: Your only persistent changes may be to the roadmap via
66
72
  the `lemming` CLI. Do NOT edit source or configuration files. Reading the
67
73
  workspace and running existing build, test, and entry-point commands for
@@ -76,10 +82,10 @@ the plan up-to-date with minimal friction.
76
82
  ## Commands
77
83
 
78
84
  ```bash
79
- # Add new tasks
80
- lemming --tasks-file {{tasks_file_path}} add '<description>' [--index N]
85
+ # Add new tasks (--oneshot for mechanical work, see directive 8)
86
+ lemming --tasks-file {{tasks_file_path}} add '<description>' [--index N] [--oneshot]
81
87
  # Edit existing tasks
82
- lemming --tasks-file {{tasks_file_path}} edit <id> --description '<desc>'
88
+ lemming --tasks-file {{tasks_file_path}} edit <id> --description '<desc>' [--oneshot]
83
89
  # Reset/Delete/Supersede/Status
84
90
  lemming --tasks-file {{tasks_file_path}} reset <id>
85
91
  lemming --tasks-file {{tasks_file_path}} delete <id>
@@ -41,6 +41,10 @@ Your CURRENT, EXCLUSIVE task is: **{{description}}**
41
41
  Include the file paths, symbols, motivation, and acceptance criteria that
42
42
  task needs, but do not restate project-wide rules already present in the
43
43
  long-term goal.
44
+ - **Mechanical Tasks Are Oneshot:** Add cleanups, renames, typo fixes,
45
+ version bumps, and scratch-file removal with `add ... --oneshot`. Each
46
+ post-task hook is a full agent run, so a hooked mechanical task takes
47
+ several times longer than it should.
44
48
  - **Detailed Evidence:** Never paste verbose gate output, transcripts, or
45
49
  error traces into a description or progress entry. Store them out of band:
46
50
  `lemming --tasks-file {{tasks_file_path}} artifact {{task_id}} <name> --file - --note '<one line>'`
@@ -187,3 +187,30 @@ def get_pending_task(data: models.Roadmap) -> models.Task | None:
187
187
 
188
188
  uncompleted.sort(key=sort_key)
189
189
  return uncompleted[0]
190
+
191
+
192
+ def milestone_reached(tasks: list[models.Task], task_id: str) -> bool | None:
193
+ """Reports whether a task, or everything that replaced it, has settled.
194
+
195
+ A superseded task counts as reached only once every replacement recorded
196
+ against it has settled too, so a milestone survives the roadmap hook
197
+ splitting it into smaller tasks.
198
+
199
+ Args:
200
+ tasks: Every task in the roadmap.
201
+ task_id: Exact ID of the milestone task.
202
+
203
+ Returns:
204
+ True when reached, False while work remains, None if the task no
205
+ longer exists.
206
+ """
207
+ task = next((t for t in tasks if t.id == task_id), None)
208
+ if task is None:
209
+ return None
210
+ if task.status == models.TaskStatus.SUPERSEDED:
211
+ replacements = [t for t in tasks if t.parent == task_id]
212
+ return all(milestone_reached(tasks, t.id) for t in replacements)
213
+ return task.status not in (
214
+ models.TaskStatus.PENDING,
215
+ models.TaskStatus.IN_PROGRESS,
216
+ )
@@ -254,3 +254,39 @@ def test_get_pending_task_none_if_in_progress(tmp_path):
254
254
  )
255
255
  pending = queries.get_pending_task(data)
256
256
  assert pending is None
257
+
258
+
259
+ def _task(task_id, status, parent=None):
260
+ return models.Task(
261
+ id=task_id, description=task_id, status=status, parent=parent
262
+ )
263
+
264
+
265
+ def test_milestone_reached_follows_terminal_status():
266
+ tasks = [_task("a", models.TaskStatus.PENDING)]
267
+ assert queries.milestone_reached(tasks, "a") is False
268
+
269
+ tasks = [_task("a", models.TaskStatus.FAILED)]
270
+ assert queries.milestone_reached(tasks, "a") is True
271
+
272
+
273
+ def test_milestone_reached_waits_for_replacements():
274
+ tasks = [
275
+ _task("a", models.TaskStatus.SUPERSEDED),
276
+ _task("b", models.TaskStatus.COMPLETED, parent="a"),
277
+ _task("c", models.TaskStatus.SUPERSEDED, parent="a"),
278
+ _task("d", models.TaskStatus.PENDING, parent="c"),
279
+ ]
280
+ assert queries.milestone_reached(tasks, "a") is False
281
+
282
+ tasks[3].status = models.TaskStatus.COMPLETED
283
+ assert queries.milestone_reached(tasks, "a") is True
284
+
285
+
286
+ def test_milestone_reached_treats_dropped_task_as_reached():
287
+ tasks = [_task("a", models.TaskStatus.SUPERSEDED)]
288
+ assert queries.milestone_reached(tasks, "a") is True
289
+
290
+
291
+ def test_milestone_reached_reports_missing_task():
292
+ assert queries.milestone_reached([], "a") is None
@@ -154,7 +154,7 @@ wheels = [
154
154
 
155
155
  [[package]]
156
156
  name = "lemming-cli"
157
- version = "0.6.1"
157
+ version = "0.7.0"
158
158
  source = { editable = "." }
159
159
  dependencies = [
160
160
  { name = "click" },
File without changes