lemming-cli 0.6.1__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/PKG-INFO +9 -1
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/README.md +8 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/SKILL.md +62 -10
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/pyproject.toml +1 -1
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/operations.py +31 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/operations_test.py +47 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/tasks.py +5 -1
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/orchestrator.py +72 -1
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/orchestrator_test.py +145 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/90-roadmap.md +9 -3
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/taskrunner.md +4 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/queries.py +27 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/queries_test.py +36 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/uv.lock +1 -1
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.dockerignore +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.github/workflows/ci.yml +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.github/workflows/publish.yml +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.github/workflows/screenshots.yml +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.gitignore +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.prettierignore +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/.prettierrc +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/Dockerfile +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/LICENSE +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/biome.json +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docker-compose.yml +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/EVALS.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/HOOKS.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/WORKSPACE.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/dashboard-desktop.png +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/dashboard-mobile.png +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/task-log-desktop.png +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/docs/screenshots/task-log-mobile.png +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/package-lock.json +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/package.json +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/playwright.config.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/auth.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/auth_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/config.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/config_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/conftest.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/context.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/context_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/directories.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/directories_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/files.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/files_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/hooks.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/hooks_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/logging.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/logging_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/loop.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/loop_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/main.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/main_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/tasks.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/api/tasks_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/artifact_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/brief_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/artifact.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/brief.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/config.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/config_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/exec_cli.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/exec_cli_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/goal.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/goal_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/hooks.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/hooks_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/machine_output_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/main.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/main_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/progress.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/progress_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/readability_cli.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/readability_cli_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/skill.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/skill_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/stop_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/cli/tasks_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/conftest.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/__main__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/cli.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/cli_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/container.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/container_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/fixtures.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/fixtures_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/harness.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/harness_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/metrics.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/metrics_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/README.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/legacy.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/base/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/clean/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/clean/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/dead-code/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/dead-code/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/duplicated-behavior/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/false-reuse/calc/limits.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/false-reuse/calc/limits_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/lint-debt/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/readability/lint-debt/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/README.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/calc/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/add-only/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/README.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/buggy-subtract/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/README.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/calc/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/full-ops/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/README.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/cli_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/roadmap/unwired-cli/calc/ops_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/hidden/stats/summary_hidden_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/README.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/stats/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/stats/summary.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/projects/task/summary/stats/summary_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/readability.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/readability_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/report.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/report_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/roadmap.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/roadmap_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/scenarios.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/scenarios_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/suites.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/task.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/task_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/trial.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/evals/trial_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/hooks.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/hooks_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/integration_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/main.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/main_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/models.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/models_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/paths.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/paths_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/persistence.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/persistence_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/50-readability.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/60-testing.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts/hooks/70-ux.md +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/prompts_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/providers.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/providers_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/runner.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/runner_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/scope.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/scope_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/shutdown.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/shutdown_integration_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/shutdown_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/__init__.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/lifecycle.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/lifecycle_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/limits.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/operations.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/operations_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/progress.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/tasks/progress_test.py +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/dashboard.spec.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/dashboard.test.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/favicon.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/favicon.spec.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/files.html +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/files.spec.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/index.html +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/index.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/logs.html +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/logs.test.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/mancha.js +0 -0
- {lemming_cli-0.6.1 → lemming_cli-0.7.0}/src/lemming/web/screenshots.spec.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lemming-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: An autonomous, iterative task runner for AI coding agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/owahltinez/lemming
|
|
6
6
|
Project-URL: Repository, https://github.com/owahltinez/lemming
|
|
@@ -445,6 +445,14 @@ These come before the subcommand and apply to all of them.
|
|
|
445
445
|
- `--yolo`: Run the runner in auto-approve mode (default: True).
|
|
446
446
|
- `--env`: Set environment variables for the runner (e.g., `--env KEY=VALUE`).
|
|
447
447
|
- `--no-defaults`: Skip default flag injection for known runners.
|
|
448
|
+
- `--until <task-id>`: Stop between tasks once that task has completed or
|
|
449
|
+
failed, printing `Reached milestone <id>; M pending.` so a supervising
|
|
450
|
+
agent can review before queueing the next milestone. Tasks inserted ahead
|
|
451
|
+
of it still run; if the roadmap hook supersedes it, the run continues until
|
|
452
|
+
every replacement has settled. Deleting it stops the run with exit 1.
|
|
453
|
+
- `--max-tasks N`: Stop between tasks once `N` have completed or failed,
|
|
454
|
+
printing `Stopped after N tasks; M pending.` Attempts that leave a task
|
|
455
|
+
pending do not count. Combine with `--until` as a budget cap.
|
|
448
456
|
- `--`: Use `--` to pass any flag directly to the underlying runner. A
|
|
449
457
|
per-task `--runner`/`--model` overrides anything passed here.
|
|
450
458
|
- **`exec [<description>]`**: Run one task, or one set of reviews, outside any
|
|
@@ -425,6 +425,14 @@ These come before the subcommand and apply to all of them.
|
|
|
425
425
|
- `--yolo`: Run the runner in auto-approve mode (default: True).
|
|
426
426
|
- `--env`: Set environment variables for the runner (e.g., `--env KEY=VALUE`).
|
|
427
427
|
- `--no-defaults`: Skip default flag injection for known runners.
|
|
428
|
+
- `--until <task-id>`: Stop between tasks once that task has completed or
|
|
429
|
+
failed, printing `Reached milestone <id>; M pending.` so a supervising
|
|
430
|
+
agent can review before queueing the next milestone. Tasks inserted ahead
|
|
431
|
+
of it still run; if the roadmap hook supersedes it, the run continues until
|
|
432
|
+
every replacement has settled. Deleting it stops the run with exit 1.
|
|
433
|
+
- `--max-tasks N`: Stop between tasks once `N` have completed or failed,
|
|
434
|
+
printing `Stopped after N tasks; M pending.` Attempts that leave a task
|
|
435
|
+
pending do not count. Combine with `--until` as a budget cap.
|
|
428
436
|
- `--`: Use `--` to pass any flag directly to the underlying runner. A
|
|
429
437
|
per-task `--runner`/`--model` overrides anything passed here.
|
|
430
438
|
- **`exec [<description>]`**: Run one task, or one set of reviews, outside any
|
|
@@ -153,33 +153,85 @@ recorded progress, so the project survives context limits.
|
|
|
153
153
|
lemming goal "Add offline support with a service worker and a sync queue"
|
|
154
154
|
lemming add "Register the service worker and cache the app shell"
|
|
155
155
|
lemming add "Queue writes in IndexedDB while offline"
|
|
156
|
+
lemming add "Bump the service worker cache version" --oneshot
|
|
156
157
|
lemming run # runs until the queue drains
|
|
158
|
+
lemming run --until <task-id> # or stops once that task settles
|
|
157
159
|
```
|
|
158
160
|
|
|
159
161
|
The loop shares one workspace across all its tasks, so isolate the **whole run**
|
|
160
162
|
— one branch or worktree created before `lemming run`, driven with
|
|
161
163
|
`lemming -C <worktree> run` — never one per task.
|
|
162
164
|
|
|
163
|
-
|
|
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
|
|
167
|
-
lemming status <id> #
|
|
168
|
-
lemming logs
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
|
@@ -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=
|
|
109
|
+
help=(
|
|
110
|
+
"Skip post-task orchestrator hooks when this task completes. Use for "
|
|
111
|
+
"mechanical work (renames, typo fixes, version bumps, cleanups); each "
|
|
112
|
+
"hook is a full agent run."
|
|
113
|
+
),
|
|
110
114
|
)
|
|
111
115
|
@click.pass_context
|
|
112
116
|
def add(
|
|
@@ -516,6 +516,54 @@ def _handle_runner_exit(
|
|
|
516
516
|
return False
|
|
517
517
|
|
|
518
518
|
|
|
519
|
+
_SETTLED_STATUSES = frozenset(
|
|
520
|
+
{
|
|
521
|
+
models.TaskStatus.COMPLETED,
|
|
522
|
+
models.TaskStatus.FAILED,
|
|
523
|
+
models.TaskStatus.CANCELLED,
|
|
524
|
+
models.TaskStatus.SUPERSEDED,
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _task_settled(tasks_file: pathlib.Path, task_id: str) -> bool:
|
|
530
|
+
"""Reports whether a task has reached a terminal status."""
|
|
531
|
+
data = persistence.load_tasks(tasks_file)
|
|
532
|
+
task = next((t for t in data.tasks if t.id == task_id), None)
|
|
533
|
+
return task is not None and task.status in _SETTLED_STATUSES
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _count_pending(data: models.Roadmap) -> int:
|
|
537
|
+
return sum(t.status == models.TaskStatus.PENDING for t in data.tasks)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _report_task_limit(data: models.Roadmap, settled: int) -> None:
|
|
541
|
+
"""Prints the one line a caller needs after a bounded run stops."""
|
|
542
|
+
task_label = "task" if settled == 1 else "tasks"
|
|
543
|
+
click.echo(
|
|
544
|
+
f"Stopped after {settled} {task_label}; {_count_pending(data)} pending."
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _check_milestone(data: models.Roadmap, until: str) -> bool | None:
|
|
549
|
+
"""Decides whether a milestone run should stop, reporting why.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
True to stop successfully, False to stop because the milestone task
|
|
553
|
+
vanished, None to keep running.
|
|
554
|
+
"""
|
|
555
|
+
reached = queries.milestone_reached(data.tasks, until)
|
|
556
|
+
if reached is None:
|
|
557
|
+
click.echo(f"Milestone task {until} no longer exists; stopping.")
|
|
558
|
+
return False
|
|
559
|
+
if reached:
|
|
560
|
+
click.echo(
|
|
561
|
+
f"Reached milestone {until}; {_count_pending(data)} pending."
|
|
562
|
+
)
|
|
563
|
+
return True
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
|
|
519
567
|
def run_loop(
|
|
520
568
|
tasks_file: pathlib.Path,
|
|
521
569
|
verbose: bool,
|
|
@@ -527,6 +575,8 @@ def run_loop(
|
|
|
527
575
|
hooks: list[str] | None = None,
|
|
528
576
|
scope: str | None = None,
|
|
529
577
|
once: bool = False,
|
|
578
|
+
max_tasks: int | None = None,
|
|
579
|
+
until: str | None = None,
|
|
530
580
|
) -> bool:
|
|
531
581
|
"""Runs pending tasks, returning True only when the roadmap is complete.
|
|
532
582
|
|
|
@@ -546,14 +596,22 @@ def run_loop(
|
|
|
546
596
|
once: Run only the first task, through its configured attempt budget.
|
|
547
597
|
Exhaustion leaves it pending, and failed finalization stops rather
|
|
548
598
|
than turning a review placeholder back into runnable work.
|
|
599
|
+
max_tasks: Stop between tasks once this many have reached a terminal
|
|
600
|
+
status, so a caller can run the roadmap one milestone at a time.
|
|
601
|
+
Attempts that leave a task pending do not count.
|
|
602
|
+
until: Exact ID of a milestone task. Stop between tasks once it, or
|
|
603
|
+
every task that replaced it, has settled; stop unsuccessfully if
|
|
604
|
+
it is deleted.
|
|
549
605
|
|
|
550
606
|
Returns:
|
|
551
|
-
True
|
|
607
|
+
True when the roadmap ran to completion, the task limit was reached,
|
|
608
|
+
or the milestone was reached.
|
|
552
609
|
"""
|
|
553
610
|
one_task_id = None
|
|
554
611
|
one_task_config = None
|
|
555
612
|
one_task_runner = None
|
|
556
613
|
one_task_model = None
|
|
614
|
+
settled_count = 0
|
|
557
615
|
while True:
|
|
558
616
|
returncode = 0
|
|
559
617
|
|
|
@@ -567,6 +625,16 @@ def run_loop(
|
|
|
567
625
|
# (e.g., from Web UI)
|
|
568
626
|
data = persistence.load_tasks(tasks_file)
|
|
569
627
|
|
|
628
|
+
# Bounded runs stop between tasks, after hooks have settled the last
|
|
629
|
+
# one, so the caller sees a consistent roadmap.
|
|
630
|
+
if max_tasks is not None and settled_count >= max_tasks:
|
|
631
|
+
_report_task_limit(data, settled_count)
|
|
632
|
+
return True
|
|
633
|
+
if until is not None:
|
|
634
|
+
milestone = _check_milestone(data, until)
|
|
635
|
+
if milestone is not None:
|
|
636
|
+
return milestone
|
|
637
|
+
|
|
570
638
|
retries = data.config.retries
|
|
571
639
|
time_limit = data.config.time_limit
|
|
572
640
|
runner_name = data.config.runner
|
|
@@ -663,6 +731,7 @@ def run_loop(
|
|
|
663
731
|
)
|
|
664
732
|
if should_abort:
|
|
665
733
|
return False
|
|
734
|
+
settled_count += _task_settled(tasks_file, task_id)
|
|
666
735
|
continue
|
|
667
736
|
|
|
668
737
|
# Add a small random jitter to avoid race conditions between
|
|
@@ -717,6 +786,7 @@ def run_loop(
|
|
|
717
786
|
(t for t in finalized.tasks if t.id == task_id), None
|
|
718
787
|
)
|
|
719
788
|
return bool(task and task.status == models.TaskStatus.COMPLETED)
|
|
789
|
+
settled_count += _task_settled(tasks_file, task_id)
|
|
720
790
|
continue
|
|
721
791
|
|
|
722
792
|
prompt = prompts.prepare_prompt(
|
|
@@ -822,6 +892,7 @@ def run_loop(
|
|
|
822
892
|
)
|
|
823
893
|
if should_abort:
|
|
824
894
|
return False
|
|
895
|
+
settled_count += _task_settled(tasks_file, task_id)
|
|
825
896
|
|
|
826
897
|
if once:
|
|
827
898
|
updated = persistence.load_tasks(tasks_file)
|
|
@@ -1053,6 +1053,151 @@ def test_run_hooks_reloads_tasks(mock_run, mock_prepare, hooks_env):
|
|
|
1053
1053
|
assert mock_load.call_count == 5
|
|
1054
1054
|
|
|
1055
1055
|
|
|
1056
|
+
def _complete_current_task(tasks_file):
|
|
1057
|
+
"""Builds a runner stub that marks whichever task it runs as completed."""
|
|
1058
|
+
|
|
1059
|
+
def side_effect(cmd, tasks_file_arg, task_id, *args, **kwargs):
|
|
1060
|
+
with persistence.lock_tasks(tasks_file):
|
|
1061
|
+
data = persistence.load_tasks(tasks_file)
|
|
1062
|
+
task = next(t for t in data.tasks if t.id == task_id)
|
|
1063
|
+
task.status = models.TaskStatus.COMPLETED
|
|
1064
|
+
task.completed_at = time.time()
|
|
1065
|
+
persistence.save_tasks(tasks_file, data)
|
|
1066
|
+
return (0, "output", "")
|
|
1067
|
+
|
|
1068
|
+
return side_effect
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
@mock.patch("lemming.runner.run_with_heartbeat")
|
|
1072
|
+
def test_run_loop_max_tasks_stops_between_tasks(mock_run, setup_env, capsys):
|
|
1073
|
+
"""A milestone limit exits cleanly and leaves the queue intact."""
|
|
1074
|
+
test_tasks_file, _ = setup_env
|
|
1075
|
+
operations.add_task(test_tasks_file, "Task 2")
|
|
1076
|
+
operations.add_task(test_tasks_file, "Task 3")
|
|
1077
|
+
mock_run.side_effect = _complete_current_task(test_tasks_file)
|
|
1078
|
+
|
|
1079
|
+
completed = run_loop(
|
|
1080
|
+
test_tasks_file,
|
|
1081
|
+
verbose=False,
|
|
1082
|
+
retry_delay=0,
|
|
1083
|
+
yolo=True,
|
|
1084
|
+
no_defaults=False,
|
|
1085
|
+
runner_args=(),
|
|
1086
|
+
hooks=[],
|
|
1087
|
+
max_tasks=2,
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
assert completed is True
|
|
1091
|
+
statuses = [t.status for t in persistence.load_tasks(test_tasks_file).tasks]
|
|
1092
|
+
assert statuses == [
|
|
1093
|
+
models.TaskStatus.COMPLETED,
|
|
1094
|
+
models.TaskStatus.COMPLETED,
|
|
1095
|
+
models.TaskStatus.PENDING,
|
|
1096
|
+
]
|
|
1097
|
+
assert "Stopped after 2 tasks; 1 pending" in capsys.readouterr().out
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
@mock.patch("lemming.runner.run_with_heartbeat")
|
|
1101
|
+
def test_run_loop_max_tasks_counts_only_settled_tasks(
|
|
1102
|
+
mock_run, setup_env, capsys
|
|
1103
|
+
):
|
|
1104
|
+
"""An attempt that leaves the task pending does not consume the budget."""
|
|
1105
|
+
test_tasks_file, _ = setup_env
|
|
1106
|
+
operations.add_task(test_tasks_file, "Task 2")
|
|
1107
|
+
attempts = []
|
|
1108
|
+
|
|
1109
|
+
def flaky_then_complete(cmd, tasks_file_arg, task_id, *args, **kwargs):
|
|
1110
|
+
attempts.append(task_id)
|
|
1111
|
+
if len(attempts) == 1:
|
|
1112
|
+
return (0, "no completion reported", "")
|
|
1113
|
+
return _complete_current_task(test_tasks_file)(
|
|
1114
|
+
cmd, tasks_file_arg, task_id, *args, **kwargs
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
mock_run.side_effect = flaky_then_complete
|
|
1118
|
+
|
|
1119
|
+
completed = run_loop(
|
|
1120
|
+
test_tasks_file,
|
|
1121
|
+
verbose=False,
|
|
1122
|
+
retry_delay=0,
|
|
1123
|
+
yolo=True,
|
|
1124
|
+
no_defaults=False,
|
|
1125
|
+
runner_args=(),
|
|
1126
|
+
hooks=[],
|
|
1127
|
+
max_tasks=1,
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
assert completed is True
|
|
1131
|
+
assert attempts == ["task1", "task1"]
|
|
1132
|
+
statuses = [t.status for t in persistence.load_tasks(test_tasks_file).tasks]
|
|
1133
|
+
assert statuses == [models.TaskStatus.COMPLETED, models.TaskStatus.PENDING]
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
@mock.patch("lemming.runner.run_with_heartbeat")
|
|
1137
|
+
def test_run_loop_until_stops_after_milestone_task(mock_run, setup_env, capsys):
|
|
1138
|
+
"""The loop runs inserted tasks and stops once the target settles."""
|
|
1139
|
+
test_tasks_file, _ = setup_env
|
|
1140
|
+
milestone = operations.add_task(test_tasks_file, "Milestone")
|
|
1141
|
+
operations.add_task(test_tasks_file, "After milestone")
|
|
1142
|
+
mock_run.side_effect = _complete_current_task(test_tasks_file)
|
|
1143
|
+
|
|
1144
|
+
completed = run_loop(
|
|
1145
|
+
test_tasks_file,
|
|
1146
|
+
verbose=False,
|
|
1147
|
+
retry_delay=0,
|
|
1148
|
+
yolo=True,
|
|
1149
|
+
no_defaults=False,
|
|
1150
|
+
runner_args=(),
|
|
1151
|
+
hooks=[],
|
|
1152
|
+
until=milestone.id,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
assert completed is True
|
|
1156
|
+
statuses = [t.status for t in persistence.load_tasks(test_tasks_file).tasks]
|
|
1157
|
+
assert statuses == [
|
|
1158
|
+
models.TaskStatus.COMPLETED,
|
|
1159
|
+
models.TaskStatus.COMPLETED,
|
|
1160
|
+
models.TaskStatus.PENDING,
|
|
1161
|
+
]
|
|
1162
|
+
assert f"Reached milestone {milestone.id}; 1 pending" in (
|
|
1163
|
+
capsys.readouterr().out
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
@mock.patch("lemming.runner.run_with_heartbeat")
|
|
1168
|
+
def test_run_loop_until_stops_when_milestone_task_vanishes(
|
|
1169
|
+
mock_run, setup_env, capsys
|
|
1170
|
+
):
|
|
1171
|
+
"""Deleting the target mid-run is reported instead of running the queue."""
|
|
1172
|
+
test_tasks_file, _ = setup_env
|
|
1173
|
+
milestone = operations.add_task(test_tasks_file, "Milestone")
|
|
1174
|
+
|
|
1175
|
+
def complete_and_delete(cmd, tasks_file_arg, task_id, *args, **kwargs):
|
|
1176
|
+
operations.delete_tasks(test_tasks_file, milestone.id)
|
|
1177
|
+
return _complete_current_task(test_tasks_file)(
|
|
1178
|
+
cmd, tasks_file_arg, task_id, *args, **kwargs
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
mock_run.side_effect = complete_and_delete
|
|
1182
|
+
|
|
1183
|
+
completed = run_loop(
|
|
1184
|
+
test_tasks_file,
|
|
1185
|
+
verbose=False,
|
|
1186
|
+
retry_delay=0,
|
|
1187
|
+
yolo=True,
|
|
1188
|
+
no_defaults=False,
|
|
1189
|
+
runner_args=(),
|
|
1190
|
+
hooks=[],
|
|
1191
|
+
until=milestone.id,
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
assert completed is False
|
|
1195
|
+
assert mock_run.call_count == 1
|
|
1196
|
+
assert f"Milestone task {milestone.id} no longer exists" in (
|
|
1197
|
+
capsys.readouterr().out
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
|
|
1056
1201
|
@mock.patch("lemming.runner.run_with_heartbeat")
|
|
1057
1202
|
def test_run_loop_records_resolved_command(mock_run, setup_env):
|
|
1058
1203
|
"""Provenance is persisted so the model behind a commit is recoverable."""
|
|
@@ -62,6 +62,12 @@ the plan up-to-date with minimal friction.
|
|
|
62
62
|
Include the concrete files, symbols, motivation, and acceptance criteria the
|
|
63
63
|
task needs, but do not repeat project-wide rules already present in the
|
|
64
64
|
long-term goal.
|
|
65
|
+
**Mechanical tasks MUST be added with `--oneshot`**: cleanups, renames,
|
|
66
|
+
typo fixes, version bumps, deleting scratch files, applying formatter
|
|
67
|
+
output, and the follow-ups from directive 7. Every hook that runs after
|
|
68
|
+
such a task costs a full agent run, often longer than the task itself.
|
|
69
|
+
Reserve hooked tasks for work that changes behavior. Mark existing
|
|
70
|
+
mechanical tasks with `edit <id> --oneshot` when you notice them.
|
|
65
71
|
9. **No Code Changes**: Your only persistent changes may be to the roadmap via
|
|
66
72
|
the `lemming` CLI. Do NOT edit source or configuration files. Reading the
|
|
67
73
|
workspace and running existing build, test, and entry-point commands for
|
|
@@ -76,10 +82,10 @@ the plan up-to-date with minimal friction.
|
|
|
76
82
|
## Commands
|
|
77
83
|
|
|
78
84
|
```bash
|
|
79
|
-
# Add new tasks
|
|
80
|
-
lemming --tasks-file {{tasks_file_path}} add '<description>' [--index N]
|
|
85
|
+
# Add new tasks (--oneshot for mechanical work, see directive 8)
|
|
86
|
+
lemming --tasks-file {{tasks_file_path}} add '<description>' [--index N] [--oneshot]
|
|
81
87
|
# Edit existing tasks
|
|
82
|
-
lemming --tasks-file {{tasks_file_path}} edit <id> --description '<desc>'
|
|
88
|
+
lemming --tasks-file {{tasks_file_path}} edit <id> --description '<desc>' [--oneshot]
|
|
83
89
|
# Reset/Delete/Supersede/Status
|
|
84
90
|
lemming --tasks-file {{tasks_file_path}} reset <id>
|
|
85
91
|
lemming --tasks-file {{tasks_file_path}} delete <id>
|
|
@@ -41,6 +41,10 @@ Your CURRENT, EXCLUSIVE task is: **{{description}}**
|
|
|
41
41
|
Include the file paths, symbols, motivation, and acceptance criteria that
|
|
42
42
|
task needs, but do not restate project-wide rules already present in the
|
|
43
43
|
long-term goal.
|
|
44
|
+
- **Mechanical Tasks Are Oneshot:** Add cleanups, renames, typo fixes,
|
|
45
|
+
version bumps, and scratch-file removal with `add ... --oneshot`. Each
|
|
46
|
+
post-task hook is a full agent run, so a hooked mechanical task takes
|
|
47
|
+
several times longer than it should.
|
|
44
48
|
- **Detailed Evidence:** Never paste verbose gate output, transcripts, or
|
|
45
49
|
error traces into a description or progress entry. Store them out of band:
|
|
46
50
|
`lemming --tasks-file {{tasks_file_path}} artifact {{task_id}} <name> --file - --note '<one line>'`
|
|
@@ -187,3 +187,30 @@ def get_pending_task(data: models.Roadmap) -> models.Task | None:
|
|
|
187
187
|
|
|
188
188
|
uncompleted.sort(key=sort_key)
|
|
189
189
|
return uncompleted[0]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def milestone_reached(tasks: list[models.Task], task_id: str) -> bool | None:
|
|
193
|
+
"""Reports whether a task, or everything that replaced it, has settled.
|
|
194
|
+
|
|
195
|
+
A superseded task counts as reached only once every replacement recorded
|
|
196
|
+
against it has settled too, so a milestone survives the roadmap hook
|
|
197
|
+
splitting it into smaller tasks.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
tasks: Every task in the roadmap.
|
|
201
|
+
task_id: Exact ID of the milestone task.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True when reached, False while work remains, None if the task no
|
|
205
|
+
longer exists.
|
|
206
|
+
"""
|
|
207
|
+
task = next((t for t in tasks if t.id == task_id), None)
|
|
208
|
+
if task is None:
|
|
209
|
+
return None
|
|
210
|
+
if task.status == models.TaskStatus.SUPERSEDED:
|
|
211
|
+
replacements = [t for t in tasks if t.parent == task_id]
|
|
212
|
+
return all(milestone_reached(tasks, t.id) for t in replacements)
|
|
213
|
+
return task.status not in (
|
|
214
|
+
models.TaskStatus.PENDING,
|
|
215
|
+
models.TaskStatus.IN_PROGRESS,
|
|
216
|
+
)
|
|
@@ -254,3 +254,39 @@ def test_get_pending_task_none_if_in_progress(tmp_path):
|
|
|
254
254
|
)
|
|
255
255
|
pending = queries.get_pending_task(data)
|
|
256
256
|
assert pending is None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _task(task_id, status, parent=None):
|
|
260
|
+
return models.Task(
|
|
261
|
+
id=task_id, description=task_id, status=status, parent=parent
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_milestone_reached_follows_terminal_status():
|
|
266
|
+
tasks = [_task("a", models.TaskStatus.PENDING)]
|
|
267
|
+
assert queries.milestone_reached(tasks, "a") is False
|
|
268
|
+
|
|
269
|
+
tasks = [_task("a", models.TaskStatus.FAILED)]
|
|
270
|
+
assert queries.milestone_reached(tasks, "a") is True
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_milestone_reached_waits_for_replacements():
|
|
274
|
+
tasks = [
|
|
275
|
+
_task("a", models.TaskStatus.SUPERSEDED),
|
|
276
|
+
_task("b", models.TaskStatus.COMPLETED, parent="a"),
|
|
277
|
+
_task("c", models.TaskStatus.SUPERSEDED, parent="a"),
|
|
278
|
+
_task("d", models.TaskStatus.PENDING, parent="c"),
|
|
279
|
+
]
|
|
280
|
+
assert queries.milestone_reached(tasks, "a") is False
|
|
281
|
+
|
|
282
|
+
tasks[3].status = models.TaskStatus.COMPLETED
|
|
283
|
+
assert queries.milestone_reached(tasks, "a") is True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_milestone_reached_treats_dropped_task_as_reached():
|
|
287
|
+
tasks = [_task("a", models.TaskStatus.SUPERSEDED)]
|
|
288
|
+
assert queries.milestone_reached(tasks, "a") is True
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_milestone_reached_reports_missing_task():
|
|
292
|
+
assert queries.milestone_reached([], "a") is None
|
|
File without changes
|