lemming-cli 0.7.0__tar.gz → 0.7.2__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.0 → lemming_cli-0.7.2}/.github/workflows/ci.yml +4 -1
  2. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/.github/workflows/screenshots.yml +1 -1
  3. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/Dockerfile +1 -1
  4. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/PKG-INFO +2 -2
  5. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/docker-compose.yml +1 -1
  6. lemming_cli-0.7.2/docs/screenshots/dashboard-desktop.png +0 -0
  7. lemming_cli-0.7.2/docs/screenshots/dashboard-mobile.png +0 -0
  8. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/pyproject.toml +2 -2
  9. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/auth.py +18 -5
  10. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/auth_test.py +10 -2
  11. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/config.py +21 -0
  12. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/config_test.py +15 -0
  13. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/tasks.py +18 -4
  14. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/tasks_test.py +18 -0
  15. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/operations.py +10 -3
  16. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/operations_test.py +17 -0
  17. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/readability.py +23 -2
  18. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/readability_test.py +47 -0
  19. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/orchestrator.py +12 -0
  20. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/paths.py +31 -8
  21. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/paths_test.py +34 -6
  22. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/runner.py +48 -0
  23. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/scope.py +4 -0
  24. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/scope_test.py +8 -0
  25. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/uv.lock +6 -6
  26. lemming_cli-0.7.0/docs/screenshots/dashboard-desktop.png +0 -0
  27. lemming_cli-0.7.0/docs/screenshots/dashboard-mobile.png +0 -0
  28. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/.dockerignore +0 -0
  29. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/.github/workflows/publish.yml +0 -0
  30. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/.gitignore +0 -0
  31. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/.prettierignore +0 -0
  32. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/.prettierrc +0 -0
  33. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/LICENSE +0 -0
  34. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/README.md +0 -0
  35. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/SKILL.md +0 -0
  36. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/biome.json +0 -0
  37. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/docs/EVALS.md +0 -0
  38. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/docs/HOOKS.md +0 -0
  39. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/docs/WORKSPACE.md +0 -0
  40. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/docs/screenshots/task-log-desktop.png +0 -0
  41. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/docs/screenshots/task-log-mobile.png +0 -0
  42. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/package-lock.json +0 -0
  43. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/package.json +0 -0
  44. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/playwright.config.js +0 -0
  45. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/__init__.py +0 -0
  46. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/__init__.py +0 -0
  47. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/conftest.py +0 -0
  48. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/context.py +0 -0
  49. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/context_test.py +0 -0
  50. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/directories.py +0 -0
  51. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/directories_test.py +0 -0
  52. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/files.py +0 -0
  53. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/files_test.py +0 -0
  54. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/hooks.py +0 -0
  55. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/hooks_test.py +0 -0
  56. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/logging.py +0 -0
  57. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/logging_test.py +0 -0
  58. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/loop.py +0 -0
  59. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/loop_test.py +0 -0
  60. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/main.py +0 -0
  61. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/api/main_test.py +0 -0
  62. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/artifact_test.py +0 -0
  63. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/brief_test.py +0 -0
  64. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/__init__.py +0 -0
  65. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/artifact.py +0 -0
  66. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/brief.py +0 -0
  67. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/config.py +0 -0
  68. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/config_test.py +0 -0
  69. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/exec_cli.py +0 -0
  70. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/exec_cli_test.py +0 -0
  71. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/goal.py +0 -0
  72. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/goal_test.py +0 -0
  73. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/hooks.py +0 -0
  74. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/hooks_test.py +0 -0
  75. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/machine_output_test.py +0 -0
  76. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/main.py +0 -0
  77. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/main_test.py +0 -0
  78. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/progress.py +0 -0
  79. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/progress_test.py +0 -0
  80. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/readability_cli.py +0 -0
  81. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/readability_cli_test.py +0 -0
  82. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/skill.py +0 -0
  83. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/skill_test.py +0 -0
  84. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/stop_test.py +0 -0
  85. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/tasks.py +0 -0
  86. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/cli/tasks_test.py +0 -0
  87. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/conftest.py +0 -0
  88. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/__init__.py +0 -0
  89. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/__main__.py +0 -0
  90. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/cli.py +0 -0
  91. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/cli_test.py +0 -0
  92. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/container.py +0 -0
  93. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/container_test.py +0 -0
  94. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/fixtures.py +0 -0
  95. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/fixtures_test.py +0 -0
  96. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/harness.py +0 -0
  97. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/harness_test.py +0 -0
  98. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/metrics.py +0 -0
  99. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/metrics_test.py +0 -0
  100. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/base/README.md +0 -0
  101. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/base/calc/__init__.py +0 -0
  102. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/base/calc/legacy.py +0 -0
  103. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/base/calc/ops.py +0 -0
  104. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/base/calc/ops_test.py +0 -0
  105. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/clean/calc/ops.py +0 -0
  106. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/clean/calc/ops_test.py +0 -0
  107. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/dead-code/calc/ops.py +0 -0
  108. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/dead-code/calc/ops_test.py +0 -0
  109. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops.py +0 -0
  110. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops_test.py +0 -0
  111. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/false-reuse/calc/limits.py +0 -0
  112. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/false-reuse/calc/limits_test.py +0 -0
  113. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/lint-debt/calc/ops.py +0 -0
  114. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/readability/lint-debt/calc/ops_test.py +0 -0
  115. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/add-only/README.md +0 -0
  116. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/add-only/calc/__init__.py +0 -0
  117. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/add-only/calc/ops.py +0 -0
  118. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/add-only/calc/ops_test.py +0 -0
  119. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/buggy-subtract/README.md +0 -0
  120. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/__init__.py +0 -0
  121. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops.py +0 -0
  122. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops_test.py +0 -0
  123. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/full-ops/README.md +0 -0
  124. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/full-ops/calc/__init__.py +0 -0
  125. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/full-ops/calc/ops.py +0 -0
  126. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/full-ops/calc/ops_test.py +0 -0
  127. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/unwired-cli/README.md +0 -0
  128. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/unwired-cli/calc/__init__.py +0 -0
  129. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli.py +0 -0
  130. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli_test.py +0 -0
  131. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops.py +0 -0
  132. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops_test.py +0 -0
  133. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/task/hidden/stats/summary_hidden_test.py +0 -0
  134. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/task/summary/README.md +0 -0
  135. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/task/summary/stats/__init__.py +0 -0
  136. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/task/summary/stats/summary.py +0 -0
  137. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/projects/task/summary/stats/summary_test.py +0 -0
  138. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/report.py +0 -0
  139. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/report_test.py +0 -0
  140. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/roadmap.py +0 -0
  141. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/roadmap_test.py +0 -0
  142. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/scenarios.py +0 -0
  143. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/scenarios_test.py +0 -0
  144. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/suites.py +0 -0
  145. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/task.py +0 -0
  146. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/task_test.py +0 -0
  147. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/trial.py +0 -0
  148. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/evals/trial_test.py +0 -0
  149. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/hooks.py +0 -0
  150. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/hooks_test.py +0 -0
  151. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/integration_test.py +0 -0
  152. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/main.py +0 -0
  153. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/main_test.py +0 -0
  154. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/models.py +0 -0
  155. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/models_test.py +0 -0
  156. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/orchestrator_test.py +0 -0
  157. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/persistence.py +0 -0
  158. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/persistence_test.py +0 -0
  159. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/prompts/hooks/50-readability.md +0 -0
  160. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/prompts/hooks/60-testing.md +0 -0
  161. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/prompts/hooks/70-ux.md +0 -0
  162. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
  163. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/prompts/taskrunner.md +0 -0
  164. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/prompts.py +0 -0
  165. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/prompts_test.py +0 -0
  166. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/providers.py +0 -0
  167. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/providers_test.py +0 -0
  168. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/runner_test.py +0 -0
  169. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/shutdown.py +0 -0
  170. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/shutdown_integration_test.py +0 -0
  171. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/shutdown_test.py +0 -0
  172. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/__init__.py +0 -0
  173. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/lifecycle.py +0 -0
  174. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/lifecycle_test.py +0 -0
  175. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/limits.py +0 -0
  176. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/operations.py +0 -0
  177. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/operations_test.py +0 -0
  178. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/progress.py +0 -0
  179. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/progress_test.py +0 -0
  180. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/queries.py +0 -0
  181. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/tasks/queries_test.py +0 -0
  182. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/dashboard.spec.js +0 -0
  183. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/dashboard.test.js +0 -0
  184. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/favicon.js +0 -0
  185. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/favicon.spec.js +0 -0
  186. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/files.html +0 -0
  187. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/files.spec.js +0 -0
  188. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/index.html +0 -0
  189. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/index.js +0 -0
  190. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/logs.html +0 -0
  191. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/logs.test.js +0 -0
  192. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/mancha.js +0 -0
  193. {lemming_cli-0.7.0 → lemming_cli-0.7.2}/src/lemming/web/screenshots.spec.js +0 -0
@@ -6,6 +6,9 @@ on:
6
6
  pull_request:
7
7
  branches: [main]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  jobs:
10
13
  lint-and-test:
11
14
  runs-on: ubuntu-latest
@@ -28,7 +31,7 @@ jobs:
28
31
 
29
32
  # Before the Python tests: they invoke readability, which runs this.
30
33
  - name: Install Node.js dependencies
31
- run: npm install
34
+ run: npm ci
32
35
 
33
36
  # Runs ruff and ruff format as before, and adds the type and prose
34
37
  # checks the separate steps never covered.
@@ -29,7 +29,7 @@ jobs:
29
29
  node-version: '20'
30
30
 
31
31
  - name: Install Node.js dependencies
32
- run: npm install
32
+ run: npm ci
33
33
 
34
34
  - name: Install Playwright browser
35
35
  run: npx playwright install --with-deps chromium
@@ -28,7 +28,7 @@ RUN uv sync --no-dev --group evals --frozen
28
28
  # the web assets. The browsers are not, and they are more than half the
29
29
  # image, so anything that will never drive one builds without them.
30
30
  ARG INSTALL_BROWSERS=true
31
- RUN npm install \
31
+ RUN npm ci \
32
32
  && if [ "$INSTALL_BROWSERS" = "true" ]; then \
33
33
  npx playwright install --with-deps; \
34
34
  fi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lemming-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
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
@@ -14,7 +14,7 @@ Requires-Dist: click>=8.0.0
14
14
  Requires-Dist: fastapi>=0.124.4
15
15
  Requires-Dist: pydantic>=2.12.5
16
16
  Requires-Dist: pyyaml>=6.0.3
17
- Requires-Dist: readability-cli>=0.10.4
17
+ Requires-Dist: readability-cli>=0.10.6
18
18
  Requires-Dist: uvicorn>=0.33.0
19
19
  Description-Content-Type: text/markdown
20
20
 
@@ -2,7 +2,7 @@ services:
2
2
  lemming:
3
3
  build: .
4
4
  ports:
5
- - '${LEMMING_PORT:-8999}:${LEMMING_PORT:-8999}'
5
+ - '127.0.0.1:${LEMMING_PORT:-8999}:${LEMMING_PORT:-8999}'
6
6
  command: ['--port', '${LEMMING_PORT:-8999}']
7
7
  volumes:
8
8
  - lemming-data:/data
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -19,7 +19,7 @@ dependencies = [
19
19
  "fastapi>=0.124.4",
20
20
  "pydantic>=2.12.5",
21
21
  "pyyaml>=6.0.3",
22
- "readability-cli>=0.10.4",
22
+ "readability-cli>=0.10.6",
23
23
  "uvicorn>=0.33.0",
24
24
  ]
25
25
 
@@ -23,6 +23,13 @@ def _token_matches(candidate: str | None, share_token: str) -> bool:
23
23
  return secrets.compare_digest(candidate.encode(), share_token.encode())
24
24
 
25
25
 
26
+ def _with_security_headers(response: fastapi.Response) -> fastapi.Response:
27
+ """Attaches baseline security headers to an outgoing response."""
28
+ response.headers.setdefault("X-Content-Type-Options", "nosniff")
29
+ response.headers.setdefault("Referrer-Policy", "no-referrer")
30
+ return response
31
+
32
+
26
33
  async def share_token_middleware(request: fastapi.Request, call_next):
27
34
  """Require a share token for every request when one is configured.
28
35
 
@@ -40,18 +47,24 @@ async def share_token_middleware(request: fastapi.Request, call_next):
40
47
  """
41
48
  share_token = getattr(request.app.state, "share_token", None)
42
49
  if not share_token:
43
- return await call_next(request)
50
+ return _with_security_headers(await call_next(request))
44
51
 
45
52
  token = request.query_params.get("token")
46
53
  if _token_matches(token, share_token):
47
54
  response = await call_next(request)
48
55
  response.set_cookie(
49
- key="lemming_share_token", value=token, httponly=True
56
+ key="lemming_share_token",
57
+ value=token,
58
+ httponly=True,
59
+ samesite="strict",
60
+ secure=request.url.scheme == "https",
50
61
  )
51
- return response
62
+ return _with_security_headers(response)
52
63
 
53
64
  cookie_token = request.cookies.get("lemming_share_token")
54
65
  if _token_matches(cookie_token, share_token):
55
- return await call_next(request)
66
+ return _with_security_headers(await call_next(request))
56
67
 
57
- return fastapi.Response("Unauthorized", status_code=401)
68
+ return _with_security_headers(
69
+ fastapi.Response("Unauthorized", status_code=401)
70
+ )
@@ -20,9 +20,17 @@ def test_share_token_middleware():
20
20
  # Valid token via query
21
21
  response = client.get("/api/data?token=secret123")
22
22
  assert response.status_code == 200
23
- assert "lemming_share_token=secret123" in response.headers.get(
24
- "set-cookie", ""
23
+ set_cookie = response.headers.get("set-cookie", "")
24
+ assert "lemming_share_token=secret123" in set_cookie
25
+ assert "samesite=strict" in set_cookie.lower()
26
+ assert response.headers.get("x-content-type-options") == "nosniff"
27
+ assert response.headers.get("referrer-policy") == "no-referrer"
28
+
29
+ https_client = fastapi.testclient.TestClient(
30
+ main.app, base_url="https://testserver"
25
31
  )
32
+ https_resp = https_client.get("/api/data?token=secret123")
33
+ assert "secure" in https_resp.headers.get("set-cookie", "").lower()
26
34
 
27
35
  # Valid token via cookie
28
36
  client.cookies.set("lemming_share_token", "secret123")
@@ -51,6 +51,18 @@ class RunRequest(pydantic.BaseModel):
51
51
  env: dict[str, str] | None = None
52
52
 
53
53
 
54
+ _BLOCKED_ENV_VARS = frozenset(
55
+ {
56
+ "LD_PRELOAD",
57
+ "LD_LIBRARY_PATH",
58
+ "DYLD_INSERT_LIBRARIES",
59
+ "PYTHONSTARTUP",
60
+ "PYTHONPATH",
61
+ "NODE_OPTIONS",
62
+ }
63
+ )
64
+
65
+
54
66
  @router.post("/api/run")
55
67
  def run_loop(
56
68
  request: fastapi.Request,
@@ -58,6 +70,15 @@ def run_loop(
58
70
  project: str | None = None,
59
71
  ):
60
72
  """Start the orchestrator loop as a detached background process."""
73
+ if run_request.env:
74
+ blocked = sorted(
75
+ key for key in run_request.env if key.upper() in _BLOCKED_ENV_VARS
76
+ )
77
+ if blocked:
78
+ raise fastapi.HTTPException(
79
+ 400, f"Forbidden environment variable(s): {', '.join(blocked)}"
80
+ )
81
+
61
82
  tasks_file = context.resolve_tasks_file(request.app.state, project)
62
83
  project_dir = context.resolve_project_dir(request.app.state, project)
63
84
 
@@ -82,3 +82,18 @@ def test_run_loop_with_project_cwd(test_workspace, client):
82
82
  # Verify Popen was called with the correct cwd
83
83
  _, kwargs = mock_popen.call_args
84
84
  assert str(kwargs["cwd"]) == str(subproject_dir)
85
+
86
+
87
+ def test_run_loop_blocks_dangerous_env_vars(client, test_tasks):
88
+ for var in (
89
+ "LD_PRELOAD",
90
+ "LD_LIBRARY_PATH",
91
+ "DYLD_INSERT_LIBRARIES",
92
+ "PYTHONSTARTUP",
93
+ "PYTHONPATH",
94
+ "NODE_OPTIONS",
95
+ ):
96
+ with patch("subprocess.Popen") as mock_popen:
97
+ response = client.post("/api/run", json={"env": {var: "evil"}})
98
+ assert response.status_code == 400, var
99
+ mock_popen.assert_not_called()
@@ -34,6 +34,17 @@ def add_task(
34
34
  request: fastapi.Request, task: AddTaskRequest, project: str | None = None
35
35
  ):
36
36
  """Add a new task and start the orchestrator loop if needed."""
37
+ if task.parent_tasks_file:
38
+ root = request.app.state.root.resolve()
39
+ lemming_home = paths.get_lemming_home().resolve()
40
+ candidate = (root / task.parent_tasks_file).resolve()
41
+ if not (
42
+ candidate.is_relative_to(root)
43
+ or candidate.is_relative_to(lemming_home)
44
+ ):
45
+ raise fastapi.HTTPException(
46
+ 400, "parent_tasks_file must be inside the workspace"
47
+ )
37
48
  tasks_file = context.resolve_tasks_file(request.app.state, project)
38
49
  try:
39
50
  new_task = operations.add_task(
@@ -200,9 +211,12 @@ def get_task_log(
200
211
  request: fastapi.Request, task_id: str, project: str | None = None
201
212
  ):
202
213
  """Get the runner log output for a task, or empty if none exists."""
203
- log_file = paths.get_log_file(
204
- context.resolve_tasks_file(request.app.state, project), task_id
205
- )
206
- if not log_file.exists():
214
+ tasks_file = context.resolve_tasks_file(request.app.state, project)
215
+ try:
216
+ paths._validate_task_id(task_id)
217
+ log_file = queries.resolve_log_file(tasks_file, task_id)
218
+ except models.TaskNotFoundError:
207
219
  return {"log": ""}
220
+ except (ValueError, models.AmbiguousTaskIdError) as e:
221
+ raise fastapi.HTTPException(400, str(e))
208
222
  return {"log": log_file.read_text(encoding="utf-8")}
@@ -511,3 +511,21 @@ def test_add_task_refuses_corrupted_tasks_file(client, test_tasks):
511
511
  assert response.status_code == 500
512
512
  assert str(test_tasks) in response.json()["detail"]
513
513
  assert test_tasks.read_text(encoding="utf-8") == corrupt
514
+
515
+
516
+ def test_add_task_rejects_outside_parent_tasks_file(client, test_tasks):
517
+ for bad_path in ("/etc/passwd", "../../outside.yml"):
518
+ response = client.post(
519
+ "/api/tasks",
520
+ json={
521
+ "description": "Child task",
522
+ "parent": "task1",
523
+ "parent_tasks_file": bad_path,
524
+ },
525
+ )
526
+ assert response.status_code == 400, bad_path
527
+
528
+
529
+ def test_get_task_log_rejects_traversal(client, test_tasks):
530
+ response = client.get("/api/tasks/..%2Fsecret/log")
531
+ assert response.status_code in (400, 404)
@@ -278,6 +278,12 @@ def serve(
278
278
  api_main.app.state.verbose = ctx.obj["VERBOSE"]
279
279
  api_main.app.state.root = pathlib.Path.cwd().resolve()
280
280
 
281
+ is_remote_host = host not in ("127.0.0.1", "::1", "localhost")
282
+ token = os.environ.get("LEMMING_SHARE_TOKEN") or (
283
+ secrets.token_urlsafe(32) if (tunnel or is_remote_host) else None
284
+ )
285
+ api_main.app.state.share_token = token
286
+
281
287
  tunnel_proc = None
282
288
  if tunnel:
283
289
  click.echo(f"[ Lemming ] Starting local server on port {port}...")
@@ -296,9 +302,6 @@ def serve(
296
302
  click.echo(f"[ Lemming ] Error starting tunnel: {e}", err=True)
297
303
  sys.exit(1)
298
304
 
299
- token = secrets.token_urlsafe(32)
300
- api_main.app.state.share_token = token
301
-
302
305
  click.echo("[ Lemming ] ")
303
306
  click.echo("[ Lemming ] ⚠️ SECURITY WARNING ")
304
307
  click.echo(
@@ -317,6 +320,10 @@ def serve(
317
320
  # The token is required locally too, so echo a usable local link.
318
321
  click.echo(f"[ Lemming ] 🖥️ Local: http://{host}:{port}?token={token}")
319
322
  click.echo("")
323
+ elif token:
324
+ click.echo(
325
+ f"Launching Lemming UI at http://{host}:{port}?token={token}"
326
+ )
320
327
  else:
321
328
  click.echo(f"Launching Lemming UI at http://{host}:{port}")
322
329
 
@@ -9,6 +9,7 @@ from unittest import mock
9
9
  import click.testing
10
10
 
11
11
  from lemming import models, persistence
12
+ from lemming.api import main as api_main
12
13
  from lemming.cli import main as cli
13
14
  from lemming.cli import operations
14
15
 
@@ -138,6 +139,22 @@ class TestCLIOperations(unittest.TestCase):
138
139
  self.assertEqual(result.exit_code, 0)
139
140
  self.assertIn("Launches the local web dashboard", result.output)
140
141
 
142
+ def test_serve_non_loopback_enables_share_token(self):
143
+ original_token = getattr(api_main.app.state, "share_token", None)
144
+ try:
145
+ with mock.patch("uvicorn.run"):
146
+ result = self.cli_runner.invoke(
147
+ cli.cli,
148
+ self.base_args + ["serve", "--host", "0.0.0.0"],
149
+ )
150
+ self.assertEqual(result.exit_code, 0)
151
+ self.assertTrue(api_main.app.state.share_token)
152
+ self.assertIn(
153
+ f"?token={api_main.app.state.share_token}", result.output
154
+ )
155
+ finally:
156
+ api_main.app.state.share_token = original_token
157
+
141
158
 
142
159
  class TestQuietPollLogConfig(unittest.TestCase):
143
160
  def test_filter_path_resolves(self):
@@ -18,11 +18,12 @@ lint-debt-repaid against clean-fast-exit.
18
18
  """
19
19
 
20
20
  import pathlib
21
+ import shutil
21
22
  import subprocess
22
23
  import sys
23
24
 
24
25
  from .. import models
25
- from . import fixtures, metrics, scenarios
26
+ from . import container, fixtures, metrics, scenarios
26
27
 
27
28
  _GOAL = (
28
29
  "Build a small calculator CLI in calc/ with add, subtract, and "
@@ -94,8 +95,28 @@ def _save_finished_task(workspace: pathlib.Path, progress: list[str]) -> None:
94
95
 
95
96
  def _check_tests_pass(workspace: pathlib.Path) -> scenarios.Check:
96
97
  """Checks that the fixture's unit tests still pass after the hook."""
98
+ if shutil.which("docker"):
99
+ cmd = [
100
+ "docker",
101
+ "run",
102
+ "--rm",
103
+ "--network=none",
104
+ "-v",
105
+ f"{workspace}:{container.WORKSPACE_MOUNT}:ro",
106
+ "-w",
107
+ container.WORKSPACE_MOUNT,
108
+ container.DEFAULT_IMAGE,
109
+ "python3",
110
+ "-m",
111
+ "unittest",
112
+ "discover",
113
+ "-p",
114
+ "*_test.py",
115
+ ]
116
+ else:
117
+ cmd = [sys.executable, "-m", "unittest", "discover", "-p", "*_test.py"]
97
118
  result = subprocess.run(
98
- [sys.executable, "-m", "unittest", "discover", "-p", "*_test.py"],
119
+ cmd,
99
120
  cwd=workspace,
100
121
  capture_output=True,
101
122
  text=True,
@@ -2,6 +2,7 @@ import pathlib
2
2
  import shutil
3
3
  import tempfile
4
4
  import unittest
5
+ import unittest.mock
5
6
 
6
7
  from lemming import models
7
8
  from lemming.evals import fixtures, readability, scenarios, suites
@@ -39,6 +40,15 @@ class ScenarioTestCase(unittest.TestCase):
39
40
  def setUp(self):
40
41
  self.workspace = pathlib.Path(tempfile.mkdtemp())
41
42
  self.addCleanup(shutil.rmtree, self.workspace, ignore_errors=True)
43
+ orig_which = shutil.which
44
+ patcher = unittest.mock.patch(
45
+ "lemming.evals.readability.shutil.which",
46
+ side_effect=lambda cmd: (
47
+ None if cmd == "docker" else orig_which(cmd)
48
+ ),
49
+ )
50
+ patcher.start()
51
+ self.addCleanup(patcher.stop)
42
52
 
43
53
  def build(self, name: str):
44
54
  self.scenario = _scenario(name)
@@ -370,6 +380,43 @@ class TestFixtureIntegrity(ScenarioTestCase):
370
380
  check = readability._check_tests_pass(workspace)
371
381
  self.assertTrue(check.passed, check.detail)
372
382
 
383
+ def test_check_tests_pass_uses_docker_when_available(self):
384
+ completed = unittest.mock.Mock(returncode=0, stderr="")
385
+ with (
386
+ unittest.mock.patch(
387
+ "lemming.evals.readability.shutil.which",
388
+ return_value="/usr/bin/docker",
389
+ ),
390
+ unittest.mock.patch(
391
+ "lemming.evals.readability.subprocess.run",
392
+ return_value=completed,
393
+ ) as mock_run,
394
+ ):
395
+ check = readability._check_tests_pass(self.workspace)
396
+
397
+ self.assertTrue(check.passed)
398
+ cmd = mock_run.call_args[0][0]
399
+ self.assertEqual(
400
+ cmd,
401
+ [
402
+ "docker",
403
+ "run",
404
+ "--rm",
405
+ "--network=none",
406
+ "-v",
407
+ f"{self.workspace}:/workspace:ro",
408
+ "-w",
409
+ "/workspace",
410
+ "lemming-evals",
411
+ "python3",
412
+ "-m",
413
+ "unittest",
414
+ "discover",
415
+ "-p",
416
+ "*_test.py",
417
+ ],
418
+ )
419
+
373
420
 
374
421
  if __name__ == "__main__":
375
422
  unittest.main()
@@ -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'.
@@ -257,6 +257,18 @@ def get_working_dir(tasks_file: pathlib.Path) -> pathlib.Path:
257
257
  return tasks_file_abs.parent
258
258
 
259
259
 
260
+ def _validate_task_id(task_id: str) -> None:
261
+ """Rejects empty or path-traversing task identifiers."""
262
+ if (
263
+ not task_id
264
+ or "/" in task_id
265
+ or "\\" in task_id
266
+ or ".." in task_id
267
+ or pathlib.Path(task_id).name != task_id
268
+ ):
269
+ raise ValueError(f"Invalid task ID: {task_id!r}")
270
+
271
+
260
272
  def get_log_file(tasks_file: pathlib.Path, task_id: str) -> pathlib.Path:
261
273
  """Returns the log file path for a specific task.
262
274
 
@@ -267,6 +279,7 @@ def get_log_file(tasks_file: pathlib.Path, task_id: str) -> pathlib.Path:
267
279
  Returns:
268
280
  A pathlib.Path to the log file for the given task.
269
281
  """
282
+ _validate_task_id(task_id)
270
283
  project_dir = get_project_dir(tasks_file)
271
284
  project_dir.mkdir(parents=True, exist_ok=True)
272
285
  return project_dir / f"{task_id}-runner.log"
@@ -286,6 +299,7 @@ def get_brief_file(tasks_file: pathlib.Path, task_id: str) -> pathlib.Path:
286
299
  Returns:
287
300
  A pathlib.Path to the brief file for the given task.
288
301
  """
302
+ _validate_task_id(task_id)
289
303
  project_dir = get_project_dir(tasks_file)
290
304
  project_dir.mkdir(parents=True, exist_ok=True)
291
305
  return project_dir / f"{task_id}-brief.md"
@@ -306,6 +320,7 @@ def get_artifacts_dir(tasks_file: pathlib.Path, task_id: str) -> pathlib.Path:
306
320
  Returns:
307
321
  A pathlib.Path to the artifacts directory, which may not exist yet.
308
322
  """
323
+ _validate_task_id(task_id)
309
324
  return get_project_dir(tasks_file) / f"{task_id}-artifacts"
310
325
 
311
326
 
@@ -342,19 +357,20 @@ def _truncate_name(name: str) -> str:
342
357
 
343
358
 
344
359
  @functools.cache
345
- def in_git_repo() -> bool:
346
- """Check if the current directory is inside a git repository.
360
+ def in_git_repo(cwd: pathlib.Path | None = None) -> bool:
361
+ """Check if a directory (or the current directory) is inside a git repo.
347
362
 
348
- The result is cached after the first call; use
349
- `in_git_repo.cache_clear()` to reset it.
363
+ Args:
364
+ cwd: Directory to check, defaulting to the current working directory.
350
365
 
351
366
  Returns:
352
367
  True if inside a git repository, False otherwise.
353
368
  """
369
+ target = cwd if cwd is not None else pathlib.Path.cwd()
354
370
  try:
355
371
  return (
356
372
  subprocess.run(
357
- ["git", "rev-parse", "--git-dir"],
373
+ ["git", "-C", str(target), "rev-parse", "--git-dir"],
358
374
  capture_output=True,
359
375
  check=False,
360
376
  ).returncode
@@ -365,7 +381,7 @@ def in_git_repo() -> bool:
365
381
 
366
382
 
367
383
  def is_ignored(path: pathlib.Path) -> bool:
368
- """Check if a given path is ignored by git.
384
+ """Check if a given path is sensitive or ignored by git.
369
385
 
370
386
  Args:
371
387
  path: The path to check for git-ignore status.
@@ -373,12 +389,19 @@ def is_ignored(path: pathlib.Path) -> bool:
373
389
  Returns:
374
390
  True if the path is ignored by git, False otherwise.
375
391
  """
376
- if not in_git_repo():
392
+ if (
393
+ ".git" in path.parts
394
+ or path.name == ".env"
395
+ or path.name.startswith(".env.")
396
+ ):
397
+ return True
398
+ cwd = path if path.is_dir() else path.parent
399
+ if not in_git_repo(cwd):
377
400
  return False
378
401
  try:
379
402
  return (
380
403
  subprocess.run(
381
- ["git", "check-ignore", "-q", str(path)],
404
+ ["git", "-C", str(cwd), "check-ignore", "-q", "--", str(path)],
382
405
  capture_output=True,
383
406
  check=False,
384
407
  ).returncode
@@ -6,20 +6,20 @@ import subprocess
6
6
  import time
7
7
  from unittest import mock
8
8
 
9
+ import pytest
10
+
9
11
  from lemming import paths
10
12
 
11
13
 
12
- def test_get_lemming_home_default():
14
+ def test_get_lemming_home_default(monkeypatch):
13
15
  expected = pathlib.Path.home() / ".local" / "lemming"
14
- if "LEMMING_HOME" in os.environ:
15
- del os.environ["LEMMING_HOME"]
16
+ monkeypatch.delenv("LEMMING_HOME", raising=False)
16
17
  assert paths.get_lemming_home() == expected
17
18
 
18
19
 
19
- def test_get_lemming_home_override(tmp_path):
20
- os.environ["LEMMING_HOME"] = str(tmp_path)
20
+ def test_get_lemming_home_override(tmp_path, monkeypatch):
21
+ monkeypatch.setenv("LEMMING_HOME", str(tmp_path))
21
22
  assert paths.get_lemming_home() == tmp_path.resolve()
22
- del os.environ["LEMMING_HOME"]
23
23
 
24
24
 
25
25
  def test_get_working_dir(tmp_path, monkeypatch):
@@ -84,6 +84,34 @@ def test_get_log_file(tmp_path):
84
84
  assert log_file.parent.exists()
85
85
 
86
86
 
87
+ def test_get_log_and_brief_file_reject_path_traversal(tmp_path):
88
+ tasks_file = tmp_path / "tasks.yml"
89
+ for bad_id in ("../escape", "a/b", "..", ""):
90
+ with pytest.raises(ValueError):
91
+ paths.get_log_file(tasks_file, bad_id)
92
+ with pytest.raises(ValueError):
93
+ paths.get_brief_file(tasks_file, bad_id)
94
+ with pytest.raises(ValueError):
95
+ paths.get_artifacts_dir(tasks_file, bad_id)
96
+
97
+
98
+ def test_is_ignored_blocks_git_and_dotenv_and_uses_target_cwd(tmp_path):
99
+ assert paths.is_ignored(tmp_path / ".git") is True
100
+ assert paths.is_ignored(tmp_path / ".git" / "config") is True
101
+ assert paths.is_ignored(tmp_path / ".env") is True
102
+ assert paths.is_ignored(tmp_path / ".env.local") is True
103
+
104
+ repo_dir = tmp_path / "subrepo"
105
+ repo_dir.mkdir()
106
+ subprocess.run(["git", "-C", str(repo_dir), "init", "-q"], check=True)
107
+ (repo_dir / ".gitignore").write_text("secret.txt\n", encoding="utf-8")
108
+ (repo_dir / "secret.txt").touch()
109
+ (repo_dir / "public.txt").touch()
110
+
111
+ assert paths.is_ignored(repo_dir / "secret.txt") is True
112
+ assert paths.is_ignored(repo_dir / "public.txt") is False
113
+
114
+
87
115
  def test_in_git_repo_and_is_ignored(tmp_path):
88
116
  orig_cwd = os.getcwd()
89
117
  os.chdir(tmp_path)