lemming-cli 0.6.1__tar.gz → 0.7.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 (191) hide show
  1. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/PKG-INFO +9 -1
  2. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/README.md +8 -0
  3. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/SKILL.md +62 -10
  4. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/pyproject.toml +1 -1
  5. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/operations.py +31 -0
  6. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/operations_test.py +47 -0
  7. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/tasks.py +5 -1
  8. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/orchestrator.py +84 -1
  9. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/orchestrator_test.py +145 -0
  10. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/prompts/hooks/90-roadmap.md +9 -3
  11. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/prompts/taskrunner.md +4 -0
  12. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/runner.py +48 -0
  13. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/queries.py +27 -0
  14. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/queries_test.py +36 -0
  15. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/uv.lock +1 -1
  16. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/.dockerignore +0 -0
  17. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/.github/workflows/ci.yml +0 -0
  18. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/.github/workflows/publish.yml +0 -0
  19. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/.github/workflows/screenshots.yml +0 -0
  20. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/.gitignore +0 -0
  21. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/.prettierignore +0 -0
  22. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/.prettierrc +0 -0
  23. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/Dockerfile +0 -0
  24. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/LICENSE +0 -0
  25. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/biome.json +0 -0
  26. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docker-compose.yml +0 -0
  27. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docs/EVALS.md +0 -0
  28. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docs/HOOKS.md +0 -0
  29. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docs/WORKSPACE.md +0 -0
  30. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docs/screenshots/dashboard-desktop.png +0 -0
  31. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docs/screenshots/dashboard-mobile.png +0 -0
  32. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docs/screenshots/task-log-desktop.png +0 -0
  33. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/docs/screenshots/task-log-mobile.png +0 -0
  34. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/package-lock.json +0 -0
  35. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/package.json +0 -0
  36. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/playwright.config.js +0 -0
  37. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/__init__.py +0 -0
  38. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/__init__.py +0 -0
  39. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/auth.py +0 -0
  40. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/auth_test.py +0 -0
  41. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/config.py +0 -0
  42. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/config_test.py +0 -0
  43. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/conftest.py +0 -0
  44. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/context.py +0 -0
  45. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/context_test.py +0 -0
  46. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/directories.py +0 -0
  47. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/directories_test.py +0 -0
  48. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/files.py +0 -0
  49. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/files_test.py +0 -0
  50. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/hooks.py +0 -0
  51. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/hooks_test.py +0 -0
  52. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/logging.py +0 -0
  53. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/logging_test.py +0 -0
  54. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/loop.py +0 -0
  55. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/loop_test.py +0 -0
  56. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/main.py +0 -0
  57. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/main_test.py +0 -0
  58. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/tasks.py +0 -0
  59. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/api/tasks_test.py +0 -0
  60. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/artifact_test.py +0 -0
  61. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/brief_test.py +0 -0
  62. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/__init__.py +0 -0
  63. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/artifact.py +0 -0
  64. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/brief.py +0 -0
  65. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/config.py +0 -0
  66. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/config_test.py +0 -0
  67. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/exec_cli.py +0 -0
  68. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/exec_cli_test.py +0 -0
  69. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/goal.py +0 -0
  70. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/goal_test.py +0 -0
  71. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/hooks.py +0 -0
  72. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/hooks_test.py +0 -0
  73. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/machine_output_test.py +0 -0
  74. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/main.py +0 -0
  75. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/main_test.py +0 -0
  76. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/progress.py +0 -0
  77. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/progress_test.py +0 -0
  78. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/readability_cli.py +0 -0
  79. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/readability_cli_test.py +0 -0
  80. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/skill.py +0 -0
  81. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/skill_test.py +0 -0
  82. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/stop_test.py +0 -0
  83. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/cli/tasks_test.py +0 -0
  84. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/conftest.py +0 -0
  85. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/__init__.py +0 -0
  86. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/__main__.py +0 -0
  87. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/cli.py +0 -0
  88. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/cli_test.py +0 -0
  89. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/container.py +0 -0
  90. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/container_test.py +0 -0
  91. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/fixtures.py +0 -0
  92. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/fixtures_test.py +0 -0
  93. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/harness.py +0 -0
  94. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/harness_test.py +0 -0
  95. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/metrics.py +0 -0
  96. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/metrics_test.py +0 -0
  97. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/base/README.md +0 -0
  98. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/base/calc/__init__.py +0 -0
  99. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/base/calc/legacy.py +0 -0
  100. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/base/calc/ops.py +0 -0
  101. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/base/calc/ops_test.py +0 -0
  102. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/clean/calc/ops.py +0 -0
  103. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/clean/calc/ops_test.py +0 -0
  104. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/dead-code/calc/ops.py +0 -0
  105. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/dead-code/calc/ops_test.py +0 -0
  106. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops.py +0 -0
  107. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops_test.py +0 -0
  108. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/false-reuse/calc/limits.py +0 -0
  109. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/false-reuse/calc/limits_test.py +0 -0
  110. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/lint-debt/calc/ops.py +0 -0
  111. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/readability/lint-debt/calc/ops_test.py +0 -0
  112. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/add-only/README.md +0 -0
  113. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/add-only/calc/__init__.py +0 -0
  114. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/add-only/calc/ops.py +0 -0
  115. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/add-only/calc/ops_test.py +0 -0
  116. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/buggy-subtract/README.md +0 -0
  117. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/__init__.py +0 -0
  118. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops.py +0 -0
  119. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops_test.py +0 -0
  120. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/full-ops/README.md +0 -0
  121. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/full-ops/calc/__init__.py +0 -0
  122. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/full-ops/calc/ops.py +0 -0
  123. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/full-ops/calc/ops_test.py +0 -0
  124. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/unwired-cli/README.md +0 -0
  125. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/__init__.py +0 -0
  126. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli.py +0 -0
  127. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli_test.py +0 -0
  128. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops.py +0 -0
  129. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops_test.py +0 -0
  130. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/task/hidden/stats/summary_hidden_test.py +0 -0
  131. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/task/summary/README.md +0 -0
  132. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/task/summary/stats/__init__.py +0 -0
  133. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/task/summary/stats/summary.py +0 -0
  134. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/projects/task/summary/stats/summary_test.py +0 -0
  135. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/readability.py +0 -0
  136. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/readability_test.py +0 -0
  137. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/report.py +0 -0
  138. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/report_test.py +0 -0
  139. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/roadmap.py +0 -0
  140. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/roadmap_test.py +0 -0
  141. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/scenarios.py +0 -0
  142. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/scenarios_test.py +0 -0
  143. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/suites.py +0 -0
  144. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/task.py +0 -0
  145. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/task_test.py +0 -0
  146. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/trial.py +0 -0
  147. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/evals/trial_test.py +0 -0
  148. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/hooks.py +0 -0
  149. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/hooks_test.py +0 -0
  150. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/integration_test.py +0 -0
  151. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/main.py +0 -0
  152. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/main_test.py +0 -0
  153. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/models.py +0 -0
  154. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/models_test.py +0 -0
  155. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/paths.py +0 -0
  156. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/paths_test.py +0 -0
  157. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/persistence.py +0 -0
  158. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/persistence_test.py +0 -0
  159. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/prompts/hooks/50-readability.md +0 -0
  160. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/prompts/hooks/60-testing.md +0 -0
  161. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/prompts/hooks/70-ux.md +0 -0
  162. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/prompts.py +0 -0
  163. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/prompts_test.py +0 -0
  164. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/providers.py +0 -0
  165. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/providers_test.py +0 -0
  166. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/runner_test.py +0 -0
  167. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/scope.py +0 -0
  168. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/scope_test.py +0 -0
  169. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/shutdown.py +0 -0
  170. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/shutdown_integration_test.py +0 -0
  171. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/shutdown_test.py +0 -0
  172. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/__init__.py +0 -0
  173. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/lifecycle.py +0 -0
  174. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/lifecycle_test.py +0 -0
  175. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/limits.py +0 -0
  176. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/operations.py +0 -0
  177. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/operations_test.py +0 -0
  178. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/progress.py +0 -0
  179. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/tasks/progress_test.py +0 -0
  180. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/dashboard.spec.js +0 -0
  181. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/dashboard.test.js +0 -0
  182. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/favicon.js +0 -0
  183. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/favicon.spec.js +0 -0
  184. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/files.html +0 -0
  185. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/files.spec.js +0 -0
  186. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/index.html +0 -0
  187. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/index.js +0 -0
  188. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/logs.html +0 -0
  189. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/logs.test.js +0 -0
  190. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/src/lemming/web/mancha.js +0 -0
  191. {lemming_cli-0.6.1 → lemming_cli-0.7.1}/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.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
@@ -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.1"
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(
@@ -392,6 +392,18 @@ def _handle_runner_exit(
392
392
  runner.RETURNCODE_TIMEOUT,
393
393
  )
394
394
 
395
+ # Record agy's silently lost file creations before hooks judge the task.
396
+ for path in runner.extract_lost_writes(stdout):
397
+ message = f"Runner failed to create {path}: agy rejected the write"
398
+ click.echo(message)
399
+ try:
400
+ progress.add_progress(
401
+ tasks_file, task_id, message[: limits.MAX_PROGRESS_ENTRY_CHARS]
402
+ )
403
+ except Exception:
404
+ # Recording the loss must never take down the loop.
405
+ pass
406
+
395
407
  # Post-execution validation and heartbeat cleanup
396
408
  # This will mark the task as COMPLETED or PENDING based on whether
397
409
  # the agent called 'lemming complete'.
@@ -516,6 +528,54 @@ def _handle_runner_exit(
516
528
  return False
517
529
 
518
530
 
531
+ _SETTLED_STATUSES = frozenset(
532
+ {
533
+ models.TaskStatus.COMPLETED,
534
+ models.TaskStatus.FAILED,
535
+ models.TaskStatus.CANCELLED,
536
+ models.TaskStatus.SUPERSEDED,
537
+ }
538
+ )
539
+
540
+
541
+ def _task_settled(tasks_file: pathlib.Path, task_id: str) -> bool:
542
+ """Reports whether a task has reached a terminal status."""
543
+ data = persistence.load_tasks(tasks_file)
544
+ task = next((t for t in data.tasks if t.id == task_id), None)
545
+ return task is not None and task.status in _SETTLED_STATUSES
546
+
547
+
548
+ def _count_pending(data: models.Roadmap) -> int:
549
+ return sum(t.status == models.TaskStatus.PENDING for t in data.tasks)
550
+
551
+
552
+ def _report_task_limit(data: models.Roadmap, settled: int) -> None:
553
+ """Prints the one line a caller needs after a bounded run stops."""
554
+ task_label = "task" if settled == 1 else "tasks"
555
+ click.echo(
556
+ f"Stopped after {settled} {task_label}; {_count_pending(data)} pending."
557
+ )
558
+
559
+
560
+ def _check_milestone(data: models.Roadmap, until: str) -> bool | None:
561
+ """Decides whether a milestone run should stop, reporting why.
562
+
563
+ Returns:
564
+ True to stop successfully, False to stop because the milestone task
565
+ vanished, None to keep running.
566
+ """
567
+ reached = queries.milestone_reached(data.tasks, until)
568
+ if reached is None:
569
+ click.echo(f"Milestone task {until} no longer exists; stopping.")
570
+ return False
571
+ if reached:
572
+ click.echo(
573
+ f"Reached milestone {until}; {_count_pending(data)} pending."
574
+ )
575
+ return True
576
+ return None
577
+
578
+
519
579
  def run_loop(
520
580
  tasks_file: pathlib.Path,
521
581
  verbose: bool,
@@ -527,6 +587,8 @@ def run_loop(
527
587
  hooks: list[str] | None = None,
528
588
  scope: str | None = None,
529
589
  once: bool = False,
590
+ max_tasks: int | None = None,
591
+ until: str | None = None,
530
592
  ) -> bool:
531
593
  """Runs pending tasks, returning True only when the roadmap is complete.
532
594
 
@@ -546,14 +608,22 @@ def run_loop(
546
608
  once: Run only the first task, through its configured attempt budget.
547
609
  Exhaustion leaves it pending, and failed finalization stops rather
548
610
  than turning a review placeholder back into runnable work.
611
+ max_tasks: Stop between tasks once this many have reached a terminal
612
+ status, so a caller can run the roadmap one milestone at a time.
613
+ Attempts that leave a task pending do not count.
614
+ until: Exact ID of a milestone task. Stop between tasks once it, or
615
+ every task that replaced it, has settled; stop unsuccessfully if
616
+ it is deleted.
549
617
 
550
618
  Returns:
551
- True only when the roadmap ran to completion.
619
+ True when the roadmap ran to completion, the task limit was reached,
620
+ or the milestone was reached.
552
621
  """
553
622
  one_task_id = None
554
623
  one_task_config = None
555
624
  one_task_runner = None
556
625
  one_task_model = None
626
+ settled_count = 0
557
627
  while True:
558
628
  returncode = 0
559
629
 
@@ -567,6 +637,16 @@ def run_loop(
567
637
  # (e.g., from Web UI)
568
638
  data = persistence.load_tasks(tasks_file)
569
639
 
640
+ # Bounded runs stop between tasks, after hooks have settled the last
641
+ # one, so the caller sees a consistent roadmap.
642
+ if max_tasks is not None and settled_count >= max_tasks:
643
+ _report_task_limit(data, settled_count)
644
+ return True
645
+ if until is not None:
646
+ milestone = _check_milestone(data, until)
647
+ if milestone is not None:
648
+ return milestone
649
+
570
650
  retries = data.config.retries
571
651
  time_limit = data.config.time_limit
572
652
  runner_name = data.config.runner
@@ -663,6 +743,7 @@ def run_loop(
663
743
  )
664
744
  if should_abort:
665
745
  return False
746
+ settled_count += _task_settled(tasks_file, task_id)
666
747
  continue
667
748
 
668
749
  # Add a small random jitter to avoid race conditions between
@@ -717,6 +798,7 @@ def run_loop(
717
798
  (t for t in finalized.tasks if t.id == task_id), None
718
799
  )
719
800
  return bool(task and task.status == models.TaskStatus.COMPLETED)
801
+ settled_count += _task_settled(tasks_file, task_id)
720
802
  continue
721
803
 
722
804
  prompt = prompts.prepare_prompt(
@@ -822,6 +904,7 @@ def run_loop(
822
904
  )
823
905
  if should_abort:
824
906
  return False
907
+ settled_count += _task_settled(tasks_file, task_id)
825
908
 
826
909
  if once:
827
910
  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>'`
@@ -398,6 +398,54 @@ def extract_error_message(
398
398
  return None
399
399
 
400
400
 
401
+ def extract_lost_writes(output: str) -> list[str]:
402
+ """Returns workspace files agy rejected creating and never wrote after.
403
+
404
+ Agy since 1.1.12 rejects a `write_to_file` carrying ArtifactMetadata for
405
+ a workspace path. The model usually retries without it, but sometimes
406
+ writes the file into agy's own artifact directory instead and still
407
+ reports success (google-antigravity/antigravity-cli#838).
408
+
409
+ Args:
410
+ output: Captured runner output, one JSON event per line.
411
+
412
+ Returns:
413
+ Rejected target paths with no later successful write, in order.
414
+ """
415
+ lost: dict[str, None] = {}
416
+ for raw_line in output.splitlines():
417
+ line = raw_line.strip()
418
+ if not line.startswith("{") or "write_to_file" not in line:
419
+ continue
420
+ try:
421
+ event = json.loads(line)
422
+ except ValueError:
423
+ continue
424
+
425
+ # Only agy's tool step updates carry the target and outcome.
426
+ update = event.get("step_update") if isinstance(event, dict) else None
427
+ if not isinstance(update, dict):
428
+ continue
429
+ if update.get("tool_name") != "write_to_file":
430
+ continue
431
+ info = update.get("tool_info")
432
+ if not isinstance(info, dict):
433
+ continue
434
+ params = info.get("parameters")
435
+ target = params.get("TargetFile") if isinstance(params, dict) else None
436
+ if not isinstance(target, str):
437
+ continue
438
+
439
+ # A later successful write to the same path recovers the rejection.
440
+ if update.get("state") == "DONE":
441
+ lost.pop(target, None)
442
+ elif update.get("state") == "ERROR" and "not a valid artifact path" in (
443
+ json.dumps(info.get("error"))
444
+ ):
445
+ lost[target] = None
446
+ return list(lost)
447
+
448
+
401
449
  def _final_message_text(event: dict) -> str | None:
402
450
  """Returns the agent's closing text from one event, or None.
403
451