lemming-cli 0.3.4__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/.gitignore +1 -0
  2. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/PKG-INFO +136 -3
  3. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/README.md +133 -0
  4. lemming_cli-0.4.0/SKILL.md +86 -0
  5. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/docs/HOOKS.md +9 -0
  6. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/pyproject.toml +7 -2
  7. lemming_cli-0.4.0/src/lemming/api/auth.py +57 -0
  8. lemming_cli-0.4.0/src/lemming/api/auth_test.py +73 -0
  9. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/main.py +20 -1
  10. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/tasks_test.py +25 -0
  11. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/__init__.py +2 -0
  12. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/brief.py +2 -2
  13. lemming_cli-0.4.0/src/lemming/cli/exec_cli.py +372 -0
  14. lemming_cli-0.4.0/src/lemming/cli/exec_cli_test.py +418 -0
  15. lemming_cli-0.4.0/src/lemming/cli/main.py +105 -0
  16. lemming_cli-0.4.0/src/lemming/cli/main_test.py +149 -0
  17. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/operations.py +11 -1
  18. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/progress.py +2 -2
  19. lemming_cli-0.4.0/src/lemming/cli/skill.py +344 -0
  20. lemming_cli-0.4.0/src/lemming/cli/skill_test.py +151 -0
  21. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/tasks.py +10 -7
  22. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/orchestrator.py +90 -5
  23. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/orchestrator_test.py +176 -0
  24. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/paths.py +70 -0
  25. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/paths_test.py +70 -0
  26. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/persistence.py +138 -17
  27. lemming_cli-0.4.0/src/lemming/persistence_test.py +378 -0
  28. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/prompts/hooks/50-readability.md +26 -15
  29. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/prompts/hooks/60-testing.md +6 -2
  30. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/prompts/hooks/70-ux.md +5 -1
  31. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/prompts.py +17 -1
  32. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/prompts_test.py +81 -0
  33. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/providers.py +20 -18
  34. lemming_cli-0.4.0/src/lemming/providers_test.py +150 -0
  35. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/runner.py +185 -17
  36. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/runner_test.py +226 -0
  37. lemming_cli-0.4.0/src/lemming/scope.py +169 -0
  38. lemming_cli-0.4.0/src/lemming/scope_test.py +117 -0
  39. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/shutdown.py +5 -0
  40. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/__init__.py +1 -0
  41. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/operations_test.py +15 -0
  42. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/dashboard.spec.js +30 -0
  43. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/index.js +11 -1
  44. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/uv.lock +4 -4
  45. lemming_cli-0.3.4/src/lemming/api/auth.py +0 -34
  46. lemming_cli-0.3.4/src/lemming/api/auth_test.py +0 -40
  47. lemming_cli-0.3.4/src/lemming/cli/main.py +0 -42
  48. lemming_cli-0.3.4/src/lemming/cli/main_test.py +0 -29
  49. lemming_cli-0.3.4/src/lemming/persistence_test.py +0 -187
  50. lemming_cli-0.3.4/src/lemming/providers_test.py +0 -73
  51. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  52. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/.github/workflows/publish.yml +0 -0
  53. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/.github/workflows/screenshots.yml +0 -0
  54. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/.prettierignore +0 -0
  55. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/.prettierrc +0 -0
  56. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/Dockerfile +0 -0
  57. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/LICENSE +0 -0
  58. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/biome.json +0 -0
  59. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/docker-compose.yml +0 -0
  60. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/docs/EVALS.md +0 -0
  61. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/docs/screenshots/dashboard-desktop.png +0 -0
  62. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/docs/screenshots/dashboard-mobile.png +0 -0
  63. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/docs/screenshots/task-log-desktop.png +0 -0
  64. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/docs/screenshots/task-log-mobile.png +0 -0
  65. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/package-lock.json +0 -0
  66. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/package.json +0 -0
  67. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/playwright.config.js +0 -0
  68. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/__init__.py +0 -0
  69. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/__init__.py +0 -0
  70. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/config.py +0 -0
  71. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/config_test.py +0 -0
  72. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/conftest.py +0 -0
  73. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/context.py +0 -0
  74. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/context_test.py +0 -0
  75. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/directories.py +0 -0
  76. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/directories_test.py +0 -0
  77. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/files.py +0 -0
  78. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/files_test.py +0 -0
  79. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/hooks.py +0 -0
  80. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/hooks_test.py +0 -0
  81. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/logging.py +0 -0
  82. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/logging_test.py +0 -0
  83. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/loop.py +0 -0
  84. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/loop_test.py +0 -0
  85. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/main_test.py +0 -0
  86. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api/tasks.py +0 -0
  87. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/api_test.py +0 -0
  88. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/brief_test.py +0 -0
  89. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/config.py +0 -0
  90. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/config_test.py +0 -0
  91. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/goal.py +0 -0
  92. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/goal_test.py +0 -0
  93. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/hooks.py +0 -0
  94. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/hooks_test.py +0 -0
  95. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/machine_output_test.py +0 -0
  96. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/operations_test.py +0 -0
  97. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/progress_test.py +0 -0
  98. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/readability_cli.py +0 -0
  99. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/readability_cli_test.py +0 -0
  100. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/stop_test.py +0 -0
  101. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli/tasks_test.py +0 -0
  102. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/cli_test.py +0 -0
  103. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/conftest.py +0 -0
  104. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/__init__.py +0 -0
  105. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/__main__.py +0 -0
  106. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/cli.py +0 -0
  107. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/cli_test.py +0 -0
  108. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/container.py +0 -0
  109. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/container_test.py +0 -0
  110. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/fixtures.py +0 -0
  111. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/fixtures_test.py +0 -0
  112. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/harness.py +0 -0
  113. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/harness_test.py +0 -0
  114. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/readability.py +0 -0
  115. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/readability_test.py +0 -0
  116. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/roadmap.py +0 -0
  117. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/roadmap_test.py +0 -0
  118. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/scenarios.py +0 -0
  119. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/scenarios_test.py +0 -0
  120. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/suites.py +0 -0
  121. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/trial.py +0 -0
  122. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/evals/trial_test.py +0 -0
  123. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/hooks.py +0 -0
  124. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/hooks_test.py +0 -0
  125. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/integration_test.py +0 -0
  126. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/main.py +0 -0
  127. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/main_test.py +0 -0
  128. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/models.py +0 -0
  129. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/models_test.py +0 -0
  130. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
  131. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/prompts/taskrunner.md +0 -0
  132. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/shutdown_integration_test.py +0 -0
  133. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/shutdown_test.py +0 -0
  134. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/lifecycle.py +0 -0
  135. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/lifecycle_test.py +0 -0
  136. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/limits.py +0 -0
  137. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/operations.py +0 -0
  138. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/progress.py +0 -0
  139. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/progress_test.py +0 -0
  140. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/queries.py +0 -0
  141. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/tasks/queries_test.py +0 -0
  142. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/dashboard.test.js +0 -0
  143. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/favicon.js +0 -0
  144. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/favicon.spec.js +0 -0
  145. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/files.html +0 -0
  146. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/files.spec.js +0 -0
  147. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/index.html +0 -0
  148. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/logs.html +0 -0
  149. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/logs.test.js +0 -0
  150. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/mancha.js +0 -0
  151. {lemming_cli-0.3.4 → lemming_cli-0.4.0}/src/lemming/web/screenshots.spec.js +0 -0
@@ -5,6 +5,7 @@ dist/
5
5
  build/
6
6
  *.egg-info/
7
7
  tasks.yml
8
+ tasks.yml.corrupt*
8
9
  *.swp
9
10
  node_modules/
10
11
  .biome/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: lemming-cli
3
- Version: 0.3.4
3
+ Version: 0.4.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
@@ -15,7 +15,7 @@ Requires-Dist: fastapi>=0.124.4
15
15
  Requires-Dist: markdownify>=0.14.1
16
16
  Requires-Dist: pydantic>=2.12.5
17
17
  Requires-Dist: pyyaml>=6.0.3
18
- Requires-Dist: readability-cli>=0.5.0
18
+ Requires-Dist: readability-cli>=0.7.0
19
19
  Requires-Dist: requests>=2.32.3
20
20
  Requires-Dist: uvicorn>=0.33.0
21
21
  Description-Content-Type: text/markdown
@@ -102,6 +102,71 @@ lemming run -- --model claude-3-5-sonnet
102
102
 
103
103
  ---
104
104
 
105
+ ## One-Off Tasks Without a Roadmap
106
+
107
+ `lemming exec` runs a single task and exits. It is the same agent-CLI
108
+ normalization the orchestrator uses, addressable on its own: name a task and a
109
+ runner, and the agent's closing message comes back on stdout.
110
+
111
+ ```bash
112
+ # Delegate one task to a different agent, e.g. to spare another's quota
113
+ lemming exec "Fix the flaky test in runner_test.py" --runner codex
114
+
115
+ # Pipe a longer handoff instead of fighting shell quoting
116
+ cat handoff.md | lemming exec -f - --runner agy
117
+ ```
118
+
119
+ With no description there is nothing for a task runner to do, so only the
120
+ reviews run — against work that already exists.
121
+
122
+ ```bash
123
+ # Review uncommitted work before opening a pull request
124
+ lemming exec --review readability
125
+
126
+ # Review someone else's branch, checked out in a worktree
127
+ lemming exec -C ../review-worktree --review testing --scope main...HEAD
128
+
129
+ # Do the work, then gate it
130
+ lemming exec "Add pagination to the tasks API" --review all
131
+ ```
132
+
133
+ `--scope` takes paths, which pass through untouched, or a git revision range,
134
+ which is resolved to the files it changed. It defaults to uncommitted work, or
135
+ to the whole tree outside a git repository.
136
+
137
+ Each run is self-contained: nothing is read from the project's roadmap or its
138
+ local hooks, one agent run is attempted, and the run's state directory is
139
+ removed unless it failed — in which case it is kept, and its path printed, so
140
+ the log can be read. Kept directories live in `~/.local/lemming/exec-*` and
141
+ are retired automatically a week later, so a recent failure is always still
142
+ there to inspect. Stdout carries the agent's message alone and everything
143
+ else goes to stderr, so the output can be consumed directly.
144
+
145
+ Note that the agent runs unattended (`--yolo` by default), so it does not
146
+ inherit the permission prompts of whatever launched it.
147
+
148
+ ### Teaching Your Agent to Use It
149
+
150
+ Lemming ships an Agent Skill so that an agent already running — Claude Code,
151
+ Gemini CLI, Cursor — can reach any other agent CLI without knowing how each
152
+ one spells its flags. Install it once:
153
+
154
+ ```bash
155
+ # Cross-tool location (~/.agents/skills), and report any tool-specific ones
156
+ lemming skill install
157
+
158
+ # Cover every agent tool detected on this machine
159
+ lemming skill install --all
160
+
161
+ # This repository only
162
+ lemming skill install --to .agents/skills
163
+ ```
164
+
165
+ `lemming skill uninstall` reverses it. Both refuse to touch a directory that
166
+ does not hold this skill, so a mistyped `--to` fails instead of deleting work.
167
+
168
+ ---
169
+
105
170
  ## The Web Dashboard
106
171
 
107
172
  Lemming includes a modern, fast Web UI to monitor your projects.
@@ -244,6 +309,17 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
244
309
 
245
310
  ## Command Reference
246
311
 
312
+ ### Global Options
313
+
314
+ These come before the subcommand and apply to all of them.
315
+
316
+ - **`-C, --project-dir <dir>`**: Run as if invoked from `<dir>`, addressing
317
+ that project's roadmap. Relative paths in other options resolve against it.
318
+ See [Working across projects](#working-across-projects).
319
+ - **`--tasks-file <path>`**: Point at a specific tasks file instead of the one
320
+ derived from the current directory.
321
+ - **`-v, --verbose`**: Show verbose output.
322
+
247
323
  ### Roadmap Management
248
324
 
249
325
  - **`status [<id>]`**: Queue/history overview or deep-dive into a specific
@@ -317,6 +393,33 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
317
393
  - `--no-defaults`: Skip default flag injection for known runners.
318
394
  - `--`: Use `--` to pass any flag directly to the underlying runner. A
319
395
  per-task `--runner`/`--model` overrides anything passed here.
396
+ - **`exec [<description>]`**: Run one task, or one set of reviews, outside any
397
+ roadmap. Prints the agent's closing message to stdout and everything else to
398
+ stderr. Exits non-zero if the task did not complete. See
399
+ [One-off tasks](#one-off-tasks-without-a-roadmap).
400
+ - `-f/--file`: Read the description from a file, or `-` for stdin. Unlike
401
+ `add`, there is no length cap.
402
+ - `--review <names>`: Reviews to run after the task, comma-separated or
403
+ repeated; `all` selects every one. With no description, only the reviews
404
+ run. Hooks that revise the roadmap cannot be selected.
405
+ - `--scope <path|range>`: What the reviews look at. Paths pass through; a
406
+ git revision range is resolved to the files it changed. Defaults to
407
+ uncommitted work, or the whole tree outside a repository.
408
+ - `--runner`, `--model`: Which agent CLI and model to use.
409
+ - `--time-limit`: Minutes before the agent is killed (default 60, 0 for no
410
+ limit).
411
+ - `--yolo/--no-yolo`: Run the agent unattended (default: True).
412
+ - `--keep`: Keep the run's state directory even when it succeeds.
413
+ - **`skill install`**: Install the packaged Agent Skill so agents discover
414
+ Lemming. Writes to `~/.agents/skills` by default and names any tool-specific
415
+ directories it found.
416
+ - `--to <dir>`: Install into a specific skills directory.
417
+ - `--all`: Also cover every detected tool's own skills directory.
418
+ - `--link`: Symlink instead of copying, so upgrades take effect immediately.
419
+ - `--force`: Replace an existing installation of this skill.
420
+ - `--dry-run`: Print what would happen, refusals included.
421
+ - **`skill uninstall`**: Remove it again. Same `--to`, `--all`, and
422
+ `--dry-run`.
320
423
  - **`stop`**: Stop the running loop and its runner.
321
424
  - `--after-current-task`: Drain instead — let the running task finish, then
322
425
  stop before claiming another. This is the safe way to change the runner
@@ -331,6 +434,36 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
331
434
 
332
435
  ---
333
436
 
437
+ ## Working across projects
438
+
439
+ When work on one project turns up something that belongs to another — a bug in
440
+ a dependency you also maintain, a doc fix in a sibling repo — file it directly
441
+ on that project's roadmap with `-C` instead of routing it through an external
442
+ issue tracker:
443
+
444
+ ```bash
445
+ # From inside project A, queue work on project B
446
+ lemming -C ~/src/other-project add "check --fix drops the trailing newline"
447
+
448
+ # Attach the evidence; the brief has no length cap
449
+ lemming -C ~/src/other-project brief <id> --file repro.md
450
+
451
+ # Read the other project's roadmap without leaving yours
452
+ lemming -C ~/src/other-project status
453
+ ```
454
+
455
+ `-C` works whether or not the target keeps a `tasks.yml` in its repo, so you
456
+ never have to know where its isolated state lives. Because it changes the
457
+ working directory, everything else follows too: the target's `.env`, its
458
+ `.lemming/hooks`, and the directory the runner executes in.
459
+
460
+ When an agent files a task this way from inside a `lemming run`, the new task
461
+ records the task it came from via `parent` and `parent_tasks_file`. The
462
+ downstream runner then sees a **Parent Task Context** section in its prompt
463
+ describing why the work was requested, so the report doesn't lose its origin.
464
+
465
+ ---
466
+
334
467
  ## Advanced: Runner Customization
335
468
 
336
469
  Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
@@ -80,6 +80,71 @@ lemming run -- --model claude-3-5-sonnet
80
80
 
81
81
  ---
82
82
 
83
+ ## One-Off Tasks Without a Roadmap
84
+
85
+ `lemming exec` runs a single task and exits. It is the same agent-CLI
86
+ normalization the orchestrator uses, addressable on its own: name a task and a
87
+ runner, and the agent's closing message comes back on stdout.
88
+
89
+ ```bash
90
+ # Delegate one task to a different agent, e.g. to spare another's quota
91
+ lemming exec "Fix the flaky test in runner_test.py" --runner codex
92
+
93
+ # Pipe a longer handoff instead of fighting shell quoting
94
+ cat handoff.md | lemming exec -f - --runner agy
95
+ ```
96
+
97
+ With no description there is nothing for a task runner to do, so only the
98
+ reviews run — against work that already exists.
99
+
100
+ ```bash
101
+ # Review uncommitted work before opening a pull request
102
+ lemming exec --review readability
103
+
104
+ # Review someone else's branch, checked out in a worktree
105
+ lemming exec -C ../review-worktree --review testing --scope main...HEAD
106
+
107
+ # Do the work, then gate it
108
+ lemming exec "Add pagination to the tasks API" --review all
109
+ ```
110
+
111
+ `--scope` takes paths, which pass through untouched, or a git revision range,
112
+ which is resolved to the files it changed. It defaults to uncommitted work, or
113
+ to the whole tree outside a git repository.
114
+
115
+ Each run is self-contained: nothing is read from the project's roadmap or its
116
+ local hooks, one agent run is attempted, and the run's state directory is
117
+ removed unless it failed — in which case it is kept, and its path printed, so
118
+ the log can be read. Kept directories live in `~/.local/lemming/exec-*` and
119
+ are retired automatically a week later, so a recent failure is always still
120
+ there to inspect. Stdout carries the agent's message alone and everything
121
+ else goes to stderr, so the output can be consumed directly.
122
+
123
+ Note that the agent runs unattended (`--yolo` by default), so it does not
124
+ inherit the permission prompts of whatever launched it.
125
+
126
+ ### Teaching Your Agent to Use It
127
+
128
+ Lemming ships an Agent Skill so that an agent already running — Claude Code,
129
+ Gemini CLI, Cursor — can reach any other agent CLI without knowing how each
130
+ one spells its flags. Install it once:
131
+
132
+ ```bash
133
+ # Cross-tool location (~/.agents/skills), and report any tool-specific ones
134
+ lemming skill install
135
+
136
+ # Cover every agent tool detected on this machine
137
+ lemming skill install --all
138
+
139
+ # This repository only
140
+ lemming skill install --to .agents/skills
141
+ ```
142
+
143
+ `lemming skill uninstall` reverses it. Both refuse to touch a directory that
144
+ does not hold this skill, so a mistyped `--to` fails instead of deleting work.
145
+
146
+ ---
147
+
83
148
  ## The Web Dashboard
84
149
 
85
150
  Lemming includes a modern, fast Web UI to monitor your projects.
@@ -222,6 +287,17 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
222
287
 
223
288
  ## Command Reference
224
289
 
290
+ ### Global Options
291
+
292
+ These come before the subcommand and apply to all of them.
293
+
294
+ - **`-C, --project-dir <dir>`**: Run as if invoked from `<dir>`, addressing
295
+ that project's roadmap. Relative paths in other options resolve against it.
296
+ See [Working across projects](#working-across-projects).
297
+ - **`--tasks-file <path>`**: Point at a specific tasks file instead of the one
298
+ derived from the current directory.
299
+ - **`-v, --verbose`**: Show verbose output.
300
+
225
301
  ### Roadmap Management
226
302
 
227
303
  - **`status [<id>]`**: Queue/history overview or deep-dive into a specific
@@ -295,6 +371,33 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
295
371
  - `--no-defaults`: Skip default flag injection for known runners.
296
372
  - `--`: Use `--` to pass any flag directly to the underlying runner. A
297
373
  per-task `--runner`/`--model` overrides anything passed here.
374
+ - **`exec [<description>]`**: Run one task, or one set of reviews, outside any
375
+ roadmap. Prints the agent's closing message to stdout and everything else to
376
+ stderr. Exits non-zero if the task did not complete. See
377
+ [One-off tasks](#one-off-tasks-without-a-roadmap).
378
+ - `-f/--file`: Read the description from a file, or `-` for stdin. Unlike
379
+ `add`, there is no length cap.
380
+ - `--review <names>`: Reviews to run after the task, comma-separated or
381
+ repeated; `all` selects every one. With no description, only the reviews
382
+ run. Hooks that revise the roadmap cannot be selected.
383
+ - `--scope <path|range>`: What the reviews look at. Paths pass through; a
384
+ git revision range is resolved to the files it changed. Defaults to
385
+ uncommitted work, or the whole tree outside a repository.
386
+ - `--runner`, `--model`: Which agent CLI and model to use.
387
+ - `--time-limit`: Minutes before the agent is killed (default 60, 0 for no
388
+ limit).
389
+ - `--yolo/--no-yolo`: Run the agent unattended (default: True).
390
+ - `--keep`: Keep the run's state directory even when it succeeds.
391
+ - **`skill install`**: Install the packaged Agent Skill so agents discover
392
+ Lemming. Writes to `~/.agents/skills` by default and names any tool-specific
393
+ directories it found.
394
+ - `--to <dir>`: Install into a specific skills directory.
395
+ - `--all`: Also cover every detected tool's own skills directory.
396
+ - `--link`: Symlink instead of copying, so upgrades take effect immediately.
397
+ - `--force`: Replace an existing installation of this skill.
398
+ - `--dry-run`: Print what would happen, refusals included.
399
+ - **`skill uninstall`**: Remove it again. Same `--to`, `--all`, and
400
+ `--dry-run`.
298
401
  - **`stop`**: Stop the running loop and its runner.
299
402
  - `--after-current-task`: Drain instead — let the running task finish, then
300
403
  stop before claiming another. This is the safe way to change the runner
@@ -309,6 +412,36 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
309
412
 
310
413
  ---
311
414
 
415
+ ## Working across projects
416
+
417
+ When work on one project turns up something that belongs to another — a bug in
418
+ a dependency you also maintain, a doc fix in a sibling repo — file it directly
419
+ on that project's roadmap with `-C` instead of routing it through an external
420
+ issue tracker:
421
+
422
+ ```bash
423
+ # From inside project A, queue work on project B
424
+ lemming -C ~/src/other-project add "check --fix drops the trailing newline"
425
+
426
+ # Attach the evidence; the brief has no length cap
427
+ lemming -C ~/src/other-project brief <id> --file repro.md
428
+
429
+ # Read the other project's roadmap without leaving yours
430
+ lemming -C ~/src/other-project status
431
+ ```
432
+
433
+ `-C` works whether or not the target keeps a `tasks.yml` in its repo, so you
434
+ never have to know where its isolated state lives. Because it changes the
435
+ working directory, everything else follows too: the target's `.env`, its
436
+ `.lemming/hooks`, and the directory the runner executes in.
437
+
438
+ When an agent files a task this way from inside a `lemming run`, the new task
439
+ records the task it came from via `parent` and `parent_tasks_file`. The
440
+ downstream runner then sees a **Parent Task Context** section in its prompt
441
+ describing why the work was requested, so the report doesn't lose its origin.
442
+
443
+ ---
444
+
312
445
  ## Advanced: Runner Customization
313
446
 
314
447
  Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: lemming
3
+ description: Delegate a single coding task to another AI agent CLI (agy, claude, codex) through one interface, or run a code review — readability, testing, UX — over changed files, a PR branch, or a path. Use when handing work to a different agent to spare a quota or play to its strengths, or when reviewing a diff before opening a pull request. Not for driving a multi-task project; that is a roadmap.
4
+ license: MIT
5
+ ---
6
+
7
+ # lemming
8
+
9
+ One interface over several agent CLIs. You do not need to know how `agy`,
10
+ `claude`, or `codex` spell their flags — only `lemming exec`.
11
+
12
+ ## Delegate one task
13
+
14
+ ```sh
15
+ lemming exec "Fix the flaky heartbeat test in src/runner_test.py" --runner codex
16
+ ```
17
+
18
+ The agent's closing message comes back on **stdout**; progress and the event
19
+ trace go to stderr. Exit code is 0 only if the task completed. That message is
20
+ the return value — read it instead of hunting through logs.
21
+
22
+ Pipe a longer handoff rather than fighting shell quoting. There is no length
23
+ limit, unlike a roadmap task description:
24
+
25
+ ```sh
26
+ cat handoff.md | lemming exec -f - --runner agy
27
+ ```
28
+
29
+ Delegate when another agent's quota, price, or strengths suit the work better
30
+ than yours. Give the task everything it needs: it starts with an empty context
31
+ and sees nothing of your conversation.
32
+
33
+ ## Run a review
34
+
35
+ With no description there is nothing for a task runner to do, so only the
36
+ reviews run — against work that already exists.
37
+
38
+ ```sh
39
+ lemming exec --review readability # uncommitted work
40
+ lemming exec --review testing --scope main...HEAD
41
+ lemming exec --review all --scope src/api/
42
+ lemming exec "Add pagination" --review all # do the work, then gate it
43
+ ```
44
+
45
+ `--scope` takes paths, which pass through untouched, or a git revision range,
46
+ which is resolved to the files it changed. It defaults to uncommitted work.
47
+ A clean tree stops the run rather than reviewing everything.
48
+
49
+ Reviews **edit the workspace**: readability applies fixes and reruns checks.
50
+ That is the point — but it means a review is not read-only.
51
+
52
+ ## Reviewing someone else's branch
53
+
54
+ Check it out in a worktree so your own tree is untouched, and point `-C` at it:
55
+
56
+ ```sh
57
+ git worktree add /tmp/pr-123 && (cd /tmp/pr-123 && gh pr checkout 123)
58
+ lemming -C /tmp/pr-123 exec --review testing --scope main...HEAD
59
+ ```
60
+
61
+ ## What to know before running it
62
+
63
+ - **The agent runs unattended.** `--yolo` is the default, so it does not
64
+ inherit the permission prompts of whatever launched it. Treat `lemming exec`
65
+ as granting an agent unsupervised write access to the working directory.
66
+ - **One agent run, no retry.** A failure is final; it does not silently try
67
+ again. `--time-limit` caps the wall clock (default 60 minutes).
68
+ - **Interrupting leaves partial edits.** The workspace is not restored, so
69
+ there is no atomicity to rely on.
70
+ - **Failures keep their log.** The state directory path is printed on stderr;
71
+ read it with `lemming logs` pointed at that tasks file.
72
+
73
+ ## Running several at once
74
+
75
+ Concurrent runs in one checkout will interleave their edits. Give each write
76
+ task its own worktree and address it with `-C`. Read-only work parallelizes
77
+ safely as-is.
78
+
79
+ ## When not to use it
80
+
81
+ `exec` is for one unit of work. Anything needing more context than a single
82
+ agent run can hold — a migration across a large codebase, a multi-step feature
83
+ — belongs on a roadmap: `lemming add`, then `lemming run`. In particular,
84
+ `--scope .` over a large repository *samples*; it does not cover.
85
+
86
+ Run `lemming exec --help` for the full flag list.
@@ -158,6 +158,9 @@ Your hook template can use the following placeholders:
158
158
  description, recent progress, and a recent execution-log excerpt capped at
159
159
  16 KiB).
160
160
  - `{{finished_task_id}}`: The ID of the task that just finished.
161
+ - `{{scope}}`: What the hook should look at, and how broadly. Under `lemming
162
+ run` this names the work the finished task left behind; under `lemming exec
163
+ --review` it is whatever `--scope` resolved to.
161
164
  - `{{tasks_file_name}}`: The filename of the tasks YAML file.
162
165
  - `{{tasks_file_path}}`: The full path to the tasks YAML file.
163
166
 
@@ -166,6 +169,12 @@ over older completed history. Readability, testing, and UX hooks receive a
166
169
  smaller roadmap summary because their primary context is the finished task and
167
170
  the workspace.
168
171
 
172
+ A review hook should take its breadth from `{{scope}}` rather than narrowing
173
+ itself in prose. Hardcoding "the files changed in the last task" makes the hook
174
+ unusable outside the orchestrator loop, where there is no last task. Depth
175
+ limits are different and belong in the prompt: "verification, not a security
176
+ audit" describes the kind of review, not its extent.
177
+
169
178
  ## Developer Ergonomics
170
179
 
171
180
  ### Listing Hooks
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.3.4"
7
+ version = "0.4.0"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "markdownify>=0.14.1",
21
21
  "pydantic>=2.12.5",
22
22
  "pyyaml>=6.0.3",
23
- "readability-cli>=0.5.0",
23
+ "readability-cli>=0.7.0",
24
24
  "requests>=2.32.3",
25
25
  "uvicorn>=0.33.0",
26
26
  ]
@@ -50,6 +50,11 @@ allow-direct-references = true
50
50
  [tool.hatch.build.targets.wheel]
51
51
  packages = ["src/lemming"]
52
52
 
53
+ # SKILL.md is authored at the repository root, where it stays visible, and
54
+ # mapped into the package so `lemming skill install` can find it in a wheel.
55
+ [tool.hatch.build.targets.wheel.force-include]
56
+ "SKILL.md" = "lemming/skills/lemming/SKILL.md"
57
+
53
58
  [tool.ruff]
54
59
  line-length = 80
55
60
 
@@ -0,0 +1,57 @@
1
+ """Share-token authentication middleware for remotely shared servers."""
2
+
3
+ import secrets
4
+
5
+ import fastapi
6
+
7
+
8
+ def _token_matches(candidate: str | None, share_token: str) -> bool:
9
+ """Compares a client-supplied token against the share token.
10
+
11
+ Args:
12
+ candidate: Token from the request, or None when absent.
13
+ share_token: The token this server requires.
14
+
15
+ Returns:
16
+ True when the candidate matches.
17
+ """
18
+ if candidate is None:
19
+ return False
20
+
21
+ # Encode first: compare_digest rejects non-ASCII str outright, which a
22
+ # client could otherwise turn into a 500 by sending one.
23
+ return secrets.compare_digest(candidate.encode(), share_token.encode())
24
+
25
+
26
+ async def share_token_middleware(request: fastapi.Request, call_next):
27
+ """Require a share token for every request when one is configured.
28
+
29
+ Accepts the token via the ``token`` query parameter (persisting it in a
30
+ cookie) or via the ``lemming_share_token`` cookie. Returns 401 when the
31
+ token is missing or invalid.
32
+
33
+ There is deliberately no local-request exemption. A token is only set in
34
+ tunnel mode, so this is already inert locally, and both a client-supplied
35
+ ``Host`` header and the peer address are forgeable or misleading behind a
36
+ tunnel (the tunnel daemon reaches the origin over loopback, so every
37
+ public request also appears to come from 127.0.0.1). Requiring the token
38
+ unconditionally keeps correctness independent of how each tunnel provider
39
+ handles those.
40
+ """
41
+ share_token = getattr(request.app.state, "share_token", None)
42
+ if not share_token:
43
+ return await call_next(request)
44
+
45
+ token = request.query_params.get("token")
46
+ if _token_matches(token, share_token):
47
+ response = await call_next(request)
48
+ response.set_cookie(
49
+ key="lemming_share_token", value=token, httponly=True
50
+ )
51
+ return response
52
+
53
+ cookie_token = request.cookies.get("lemming_share_token")
54
+ if _token_matches(cookie_token, share_token):
55
+ return await call_next(request)
56
+
57
+ return fastapi.Response("Unauthorized", status_code=401)
@@ -0,0 +1,73 @@
1
+ import fastapi.testclient
2
+
3
+ from lemming import api
4
+
5
+
6
+ def test_share_token_middleware():
7
+ # Setup test client
8
+ original_token = getattr(api.app.state, "share_token", None)
9
+ try:
10
+ api.app.state.share_token = "secret123"
11
+ # We need a fresh client for each test that modifies app state
12
+ # middleware if it uses the app state, but here TestClient is
13
+ # created with api.app
14
+ client = fastapi.testclient.TestClient(api.app)
15
+
16
+ # Missing token -> 401
17
+ response = client.get("/api/data")
18
+ assert response.status_code == 401
19
+
20
+ # Valid token via query
21
+ response = client.get("/api/data?token=secret123")
22
+ assert response.status_code == 200
23
+ assert "lemming_share_token=secret123" in response.headers.get(
24
+ "set-cookie", ""
25
+ )
26
+
27
+ # Valid token via cookie
28
+ client.cookies.set("lemming_share_token", "secret123")
29
+ response = client.get("/api/data")
30
+ assert response.status_code == 200
31
+
32
+ # A client-supplied local host header must not bypass the token
33
+ client.cookies.clear()
34
+ for host in ("127.0.0.1:8999", "localhost:8999", "localhost"):
35
+ response = client.get("/api/data", headers={"host": host})
36
+ assert response.status_code == 401, host
37
+ finally:
38
+ # Restore
39
+ api.app.state.share_token = original_token
40
+
41
+
42
+ def test_share_token_middleware_rejects_exotic_tokens():
43
+ """Odd tokens must be rejected, not crash the comparison."""
44
+ original_token = getattr(api.app.state, "share_token", None)
45
+ try:
46
+ api.app.state.share_token = "secret123"
47
+ client = fastapi.testclient.TestClient(api.app)
48
+
49
+ # Non-ASCII and differing lengths must stay a clean 401. A digest
50
+ # comparison rejects both only if the values are encoded first.
51
+ for token in ("sécret123", "secret1234567890", "s", ""):
52
+ response = client.get("/api/data", params={"token": token})
53
+ assert response.status_code == 401, token
54
+ finally:
55
+ # Restore
56
+ api.app.state.share_token = original_token
57
+
58
+
59
+ def test_share_token_middleware_inert_without_token():
60
+ """Local (non-tunnel) mode sets no share token, so nothing is required."""
61
+ original_token = getattr(api.app.state, "share_token", None)
62
+ try:
63
+ api.app.state.share_token = None
64
+ client = fastapi.testclient.TestClient(api.app)
65
+
66
+ response = client.get("/api/data")
67
+ assert response.status_code == 200
68
+ assert "lemming_share_token" not in response.headers.get(
69
+ "set-cookie", ""
70
+ )
71
+ finally:
72
+ # Restore
73
+ api.app.state.share_token = original_token
@@ -1,16 +1,19 @@
1
1
  """FastAPI application setup: middleware, routers, and static files."""
2
2
 
3
3
  import importlib.resources
4
+ import logging
4
5
  import pathlib
5
6
 
6
7
  import fastapi
7
8
  import fastapi.responses
8
9
  import fastapi.staticfiles
9
10
 
10
- from .. import paths
11
+ from .. import paths, persistence
11
12
  from . import auth, config, directories, files, hooks, tasks
12
13
  from . import logging as lemming_logging
13
14
 
15
+ logger = logging.getLogger(__name__)
16
+
14
17
  # Re-exported so logging configs can reference lemming.api.QuietPollFilter
15
18
  QuietPollFilter = lemming_logging.QuietPollFilter
16
19
 
@@ -40,6 +43,22 @@ app.include_router(directories.router)
40
43
  app.include_router(hooks.router)
41
44
  app.include_router(config.router)
42
45
 
46
+
47
+ @app.exception_handler(persistence.CorruptedTasksError)
48
+ def handle_corrupted_tasks(
49
+ request: fastapi.Request, exc: Exception
50
+ ) -> fastapi.responses.JSONResponse:
51
+ """Reports an unreadable tasks file in the shape the UI already expects.
52
+
53
+ Without this every route touching the roadmap would answer a corrupt file
54
+ with a raw stack trace instead of an actionable message.
55
+ """
56
+ logger.error("%s could not read the tasks file: %s", request.url.path, exc)
57
+ return fastapi.responses.JSONResponse(
58
+ status_code=500, content={"detail": str(exc)}
59
+ )
60
+
61
+
43
62
  # Static files and root routes
44
63
  web_dir = pathlib.Path(
45
64
  str(importlib.resources.files("lemming").joinpath("web"))