lemming-cli 0.1.3__tar.gz → 0.1.4__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 (129) hide show
  1. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/PKG-INFO +1 -1
  2. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/pyproject.toml +1 -1
  3. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/orchestrator.py +24 -0
  4. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/orchestrator_test.py +69 -0
  5. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/__init__.py +3 -0
  6. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/lifecycle.py +31 -0
  7. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/uv.lock +1 -1
  8. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/.github/workflows/ci.yml +0 -0
  9. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/.github/workflows/publish.yml +0 -0
  10. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/.github/workflows/screenshots.yml +0 -0
  11. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/.gitignore +0 -0
  12. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/.prettierignore +0 -0
  13. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/.prettierrc +0 -0
  14. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/Dockerfile +0 -0
  15. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/LICENSE +0 -0
  16. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/README.md +0 -0
  17. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/biome.json +0 -0
  18. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/docker-compose.yml +0 -0
  19. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/docs/EVALS.md +0 -0
  20. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/docs/HOOKS.md +0 -0
  21. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/docs/screenshots/dashboard-desktop.png +0 -0
  22. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/docs/screenshots/dashboard-mobile.png +0 -0
  23. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/docs/screenshots/task-log-desktop.png +0 -0
  24. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/docs/screenshots/task-log-mobile.png +0 -0
  25. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/package-lock.json +0 -0
  26. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/package.json +0 -0
  27. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/playwright.config.js +0 -0
  28. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/__init__.py +0 -0
  29. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/__init__.py +0 -0
  30. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/auth.py +0 -0
  31. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/auth_test.py +0 -0
  32. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/config.py +0 -0
  33. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/config_test.py +0 -0
  34. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/conftest.py +0 -0
  35. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/context.py +0 -0
  36. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/context_test.py +0 -0
  37. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/directories.py +0 -0
  38. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/directories_test.py +0 -0
  39. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/files.py +0 -0
  40. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/files_test.py +0 -0
  41. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/hooks.py +0 -0
  42. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/hooks_test.py +0 -0
  43. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/logging.py +0 -0
  44. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/logging_test.py +0 -0
  45. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/loop.py +0 -0
  46. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/loop_test.py +0 -0
  47. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/main.py +0 -0
  48. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/main_test.py +0 -0
  49. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/tasks.py +0 -0
  50. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api/tasks_test.py +0 -0
  51. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/api_test.py +0 -0
  52. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/__init__.py +0 -0
  53. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/config.py +0 -0
  54. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/config_test.py +0 -0
  55. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/goal.py +0 -0
  56. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/goal_test.py +0 -0
  57. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/hooks.py +0 -0
  58. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/hooks_test.py +0 -0
  59. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/main.py +0 -0
  60. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/main_test.py +0 -0
  61. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/operations.py +0 -0
  62. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/operations_test.py +0 -0
  63. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/progress.py +0 -0
  64. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/progress_test.py +0 -0
  65. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/readability_cli.py +0 -0
  66. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/readability_cli_test.py +0 -0
  67. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/tasks.py +0 -0
  68. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli/tasks_test.py +0 -0
  69. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/cli_test.py +0 -0
  70. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/conftest.py +0 -0
  71. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/__init__.py +0 -0
  72. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/__main__.py +0 -0
  73. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/cli.py +0 -0
  74. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/cli_test.py +0 -0
  75. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/container.py +0 -0
  76. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/container_test.py +0 -0
  77. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/fixtures.py +0 -0
  78. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/fixtures_test.py +0 -0
  79. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/harness.py +0 -0
  80. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/harness_test.py +0 -0
  81. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/readability.py +0 -0
  82. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/readability_test.py +0 -0
  83. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/roadmap.py +0 -0
  84. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/roadmap_test.py +0 -0
  85. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/scenarios.py +0 -0
  86. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/scenarios_test.py +0 -0
  87. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/suites.py +0 -0
  88. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/trial.py +0 -0
  89. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/evals/trial_test.py +0 -0
  90. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/hooks.py +0 -0
  91. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/hooks_test.py +0 -0
  92. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/integration_test.py +0 -0
  93. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/main.py +0 -0
  94. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/main_test.py +0 -0
  95. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/models.py +0 -0
  96. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/models_test.py +0 -0
  97. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/paths.py +0 -0
  98. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/paths_test.py +0 -0
  99. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/persistence.py +0 -0
  100. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/persistence_test.py +0 -0
  101. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/prompts/hooks/50-readability.md +0 -0
  102. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/prompts/hooks/60-testing.md +0 -0
  103. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
  104. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/prompts/taskrunner.md +0 -0
  105. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/prompts.py +0 -0
  106. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/prompts_test.py +0 -0
  107. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/providers.py +0 -0
  108. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/providers_test.py +0 -0
  109. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/runner.py +0 -0
  110. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/runner_test.py +0 -0
  111. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/lifecycle_test.py +0 -0
  112. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/operations.py +0 -0
  113. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/operations_test.py +0 -0
  114. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/progress.py +0 -0
  115. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/progress_test.py +0 -0
  116. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/queries.py +0 -0
  117. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/tasks/queries_test.py +0 -0
  118. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/dashboard.spec.js +0 -0
  119. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/dashboard.test.js +0 -0
  120. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/favicon.js +0 -0
  121. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/favicon.spec.js +0 -0
  122. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/files.html +0 -0
  123. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/files.spec.js +0 -0
  124. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/index.html +0 -0
  125. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/index.js +0 -0
  126. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/logs.html +0 -0
  127. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/logs.test.js +0 -0
  128. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/mancha.js +0 -0
  129. {lemming_cli-0.1.3 → lemming_cli-0.1.4}/src/lemming/web/screenshots.spec.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lemming-cli
3
- Version: 0.1.3
3
+ Version: 0.1.4
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -140,6 +140,30 @@ def run_hooks(
140
140
  current = next((t for t in data.tasks if t.id == task_id), None)
141
141
  if current and current.status != tasks.TaskStatus.IN_PROGRESS:
142
142
  return exit_codes
143
+
144
+ # If any hook run was killed or crashed (e.g. the model became
145
+ # unavailable mid-finalization), the completion is unverified.
146
+ # Revert the task to pending — keeping its attempt count — so
147
+ # the whole task is retried instead of being silently marked
148
+ # completed. Failure finalization proceeds regardless: those
149
+ # tasks have already exhausted their attempts.
150
+ failed_hooks = [h for h, code in exit_codes.items() if code != 0]
151
+ if final_status == tasks.TaskStatus.COMPLETED and failed_hooks:
152
+ tasks.add_progress(
153
+ tasks_file,
154
+ task_id,
155
+ "Finalization hooks failed "
156
+ f"({', '.join(failed_hooks)}); task reverted to "
157
+ "pending for retry.",
158
+ )
159
+ tasks.revert_task_to_pending(tasks_file, task_id)
160
+ click.echo(
161
+ f"Task {task_id} finalization hooks failed "
162
+ f"({', '.join(failed_hooks)}). Reverting to pending "
163
+ "for retry."
164
+ )
165
+ return exit_codes
166
+
143
167
  tasks.update_task(
144
168
  tasks_file, task_id, status=final_status, force=True
145
169
  )
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import time
2
3
  from unittest import mock
3
4
 
@@ -689,6 +690,74 @@ def test_run_hooks_skips_finalization_when_healed(
689
690
  assert data.tasks[0].attempts == 0
690
691
 
691
692
 
693
+ @mock.patch("lemming.prompts.prepare_hook_prompt")
694
+ @mock.patch("lemming.runner.run_with_heartbeat")
695
+ def test_run_hooks_reverts_to_pending_when_hook_killed(
696
+ mock_run, mock_prepare, hooks_env
697
+ ):
698
+ """A killed/failed finalization hook must not mark the task completed."""
699
+ mock_prepare.return_value = "Mock Prompt"
700
+ # Simulate the hook runner being killed (e.g. model became unavailable).
701
+ mock_run.return_value = (-15, "", "")
702
+
703
+ # Put the task in the finalizing state: in progress with a requested
704
+ # completion, as if the agent had called `lemming complete`.
705
+ tasks.update_task(
706
+ hooks_env, "12345678", status=tasks.TaskStatus.IN_PROGRESS
707
+ )
708
+ tasks.claim_task(hooks_env, "12345678", pid=os.getpid())
709
+ tasks.update_task(hooks_env, "12345678", status=tasks.TaskStatus.COMPLETED)
710
+
711
+ run_hooks(
712
+ hooks_env,
713
+ "12345678",
714
+ "agy",
715
+ yolo=True,
716
+ runner_args=(),
717
+ no_defaults=False,
718
+ verbose=False,
719
+ hooks=["roadmap"],
720
+ final_status=tasks.TaskStatus.COMPLETED,
721
+ )
722
+
723
+ # The task should be retried from scratch, keeping its attempt count.
724
+ data = tasks.load_tasks(hooks_env)
725
+ task = data.tasks[0]
726
+ assert task.status == tasks.TaskStatus.PENDING
727
+ assert task.requested_status is None
728
+ assert task.attempts == 1
729
+ assert any("hook" in p.lower() for p in task.progress)
730
+
731
+
732
+ @mock.patch("lemming.prompts.prepare_hook_prompt")
733
+ @mock.patch("lemming.runner.run_with_heartbeat")
734
+ def test_run_hooks_failure_finalization_ignores_hook_errors(
735
+ mock_run, mock_prepare, hooks_env
736
+ ):
737
+ """Failed hooks must not revert a task that is being marked FAILED."""
738
+ mock_prepare.return_value = "Mock Prompt"
739
+ mock_run.return_value = (1, "", "")
740
+
741
+ tasks.update_task(
742
+ hooks_env, "12345678", status=tasks.TaskStatus.IN_PROGRESS
743
+ )
744
+
745
+ run_hooks(
746
+ hooks_env,
747
+ "12345678",
748
+ "agy",
749
+ yolo=True,
750
+ runner_args=(),
751
+ no_defaults=False,
752
+ verbose=False,
753
+ hooks=["roadmap"],
754
+ final_status=tasks.TaskStatus.FAILED,
755
+ )
756
+
757
+ data = tasks.load_tasks(hooks_env)
758
+ assert data.tasks[0].status == tasks.TaskStatus.FAILED
759
+
760
+
692
761
  @mock.patch("lemming.prompts.prepare_hook_prompt")
693
762
  @mock.patch("lemming.runner.run_with_heartbeat")
694
763
  def test_run_hooks_reloads_tasks(mock_run, mock_prepare, hooks_env):
@@ -31,6 +31,9 @@ from .lifecycle import (
31
31
  from .lifecycle import (
32
32
  reset_task_logs as reset_task_logs,
33
33
  )
34
+ from .lifecycle import (
35
+ revert_task_to_pending as revert_task_to_pending,
36
+ )
34
37
  from .lifecycle import (
35
38
  update_heartbeat as update_heartbeat,
36
39
  )
@@ -218,6 +218,37 @@ def finish_task_attempt(
218
218
  return task
219
219
 
220
220
 
221
+ def revert_task_to_pending(
222
+ tasks_file: pathlib.Path, task_id: str
223
+ ) -> models.Task | None:
224
+ """Reverts an in-progress or finalizing task to pending for a retry.
225
+
226
+ Unlike reset_task, this preserves the attempt count and progress so
227
+ retry accounting still applies.
228
+
229
+ Args:
230
+ tasks_file: Path to the tasks YAML file.
231
+ task_id: The ID of the task to revert.
232
+
233
+ Returns:
234
+ The updated Task, or None if not found.
235
+ """
236
+ with persistence.lock_tasks(tasks_file):
237
+ data = persistence.load_tasks(tasks_file)
238
+ task = next((t for t in data.tasks if t.id == task_id), None)
239
+ if not task:
240
+ return None
241
+
242
+ update_run_time(task)
243
+ task.status = models.TaskStatus.PENDING
244
+ task.requested_status = None
245
+ task.pid = None
246
+ task.last_heartbeat = None
247
+
248
+ persistence.save_tasks(tasks_file, data)
249
+ return task
250
+
251
+
221
252
  def update_heartbeat(
222
253
  tasks_file: pathlib.Path, task_id: str, pid: int | None = None
223
254
  ) -> bool:
@@ -222,7 +222,7 @@ wheels = [
222
222
 
223
223
  [[package]]
224
224
  name = "lemming-cli"
225
- version = "0.1.3"
225
+ version = "0.1.4"
226
226
  source = { editable = "." }
227
227
  dependencies = [
228
228
  { name = "beautifulsoup4" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes