lemming-cli 0.1.2__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 (139) hide show
  1. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/PKG-INFO +22 -21
  2. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/README.md +21 -20
  3. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/docs/HOOKS.md +45 -41
  4. lemming_cli-0.1.4/docs/screenshots/dashboard-desktop.png +0 -0
  5. lemming_cli-0.1.4/docs/screenshots/dashboard-mobile.png +0 -0
  6. lemming_cli-0.1.4/docs/screenshots/task-log-desktop.png +0 -0
  7. lemming_cli-0.1.4/docs/screenshots/task-log-mobile.png +0 -0
  8. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/pyproject.toml +1 -1
  9. lemming_cli-0.1.4/src/lemming/api/hooks.py +57 -0
  10. lemming_cli-0.1.4/src/lemming/api/hooks_test.py +92 -0
  11. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/config.py +5 -2
  12. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/config_test.py +3 -0
  13. lemming_cli-0.1.4/src/lemming/cli/hooks.py +85 -0
  14. lemming_cli-0.1.4/src/lemming/cli/hooks_test.py +123 -0
  15. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/readability.py +0 -1
  16. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/roadmap.py +1 -1
  17. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/trial.py +1 -1
  18. lemming_cli-0.1.4/src/lemming/hooks.py +238 -0
  19. lemming_cli-0.1.4/src/lemming/hooks_test.py +224 -0
  20. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/models.py +0 -1
  21. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/models_test.py +0 -1
  22. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/orchestrator.py +174 -4
  23. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/orchestrator_test.py +317 -1
  24. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/paths.py +4 -2
  25. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/paths_test.py +34 -1
  26. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/prompts.py +17 -105
  27. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/prompts_test.py +20 -58
  28. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/runner.py +5 -3
  29. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/runner_test.py +5 -4
  30. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/__init__.py +3 -0
  31. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/lifecycle.py +31 -0
  32. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/dashboard.spec.js +9 -1
  33. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/dashboard.test.js +0 -1
  34. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/favicon.spec.js +9 -1
  35. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/index.html +5 -16
  36. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/index.js +16 -15
  37. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/screenshots.spec.js +23 -2
  38. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/uv.lock +1 -1
  39. lemming_cli-0.1.2/docs/screenshots/dashboard-desktop.png +0 -0
  40. lemming_cli-0.1.2/docs/screenshots/dashboard-mobile.png +0 -0
  41. lemming_cli-0.1.2/docs/screenshots/task-log-desktop.png +0 -0
  42. lemming_cli-0.1.2/docs/screenshots/task-log-mobile.png +0 -0
  43. lemming_cli-0.1.2/src/lemming/api/hooks.py +0 -15
  44. lemming_cli-0.1.2/src/lemming/api/hooks_test.py +0 -33
  45. lemming_cli-0.1.2/src/lemming/cli/hooks.py +0 -147
  46. lemming_cli-0.1.2/src/lemming/cli/hooks_test.py +0 -50
  47. lemming_cli-0.1.2/src/lemming/hooks.py +0 -154
  48. lemming_cli-0.1.2/src/lemming/hooks_test.py +0 -227
  49. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.github/workflows/ci.yml +0 -0
  50. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.github/workflows/publish.yml +0 -0
  51. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.github/workflows/screenshots.yml +0 -0
  52. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.gitignore +0 -0
  53. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.prettierignore +0 -0
  54. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.prettierrc +0 -0
  55. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/Dockerfile +0 -0
  56. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/LICENSE +0 -0
  57. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/biome.json +0 -0
  58. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/docker-compose.yml +0 -0
  59. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/docs/EVALS.md +0 -0
  60. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/package-lock.json +0 -0
  61. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/package.json +0 -0
  62. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/playwright.config.js +0 -0
  63. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/__init__.py +0 -0
  64. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/__init__.py +0 -0
  65. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/auth.py +0 -0
  66. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/auth_test.py +0 -0
  67. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/config.py +0 -0
  68. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/config_test.py +0 -0
  69. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/conftest.py +0 -0
  70. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/context.py +0 -0
  71. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/context_test.py +0 -0
  72. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/directories.py +0 -0
  73. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/directories_test.py +0 -0
  74. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/files.py +0 -0
  75. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/files_test.py +0 -0
  76. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/logging.py +0 -0
  77. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/logging_test.py +0 -0
  78. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/loop.py +0 -0
  79. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/loop_test.py +0 -0
  80. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/main.py +0 -0
  81. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/main_test.py +0 -0
  82. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/tasks.py +0 -0
  83. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/tasks_test.py +0 -0
  84. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api_test.py +0 -0
  85. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/__init__.py +0 -0
  86. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/goal.py +0 -0
  87. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/goal_test.py +0 -0
  88. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/main.py +0 -0
  89. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/main_test.py +0 -0
  90. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/operations.py +0 -0
  91. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/operations_test.py +0 -0
  92. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/progress.py +0 -0
  93. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/progress_test.py +0 -0
  94. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/readability_cli.py +0 -0
  95. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/readability_cli_test.py +0 -0
  96. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/tasks.py +0 -0
  97. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/tasks_test.py +0 -0
  98. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli_test.py +0 -0
  99. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/conftest.py +0 -0
  100. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/__init__.py +0 -0
  101. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/__main__.py +0 -0
  102. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/cli.py +0 -0
  103. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/cli_test.py +0 -0
  104. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/container.py +0 -0
  105. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/container_test.py +0 -0
  106. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/fixtures.py +0 -0
  107. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/fixtures_test.py +0 -0
  108. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/harness.py +0 -0
  109. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/harness_test.py +0 -0
  110. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/readability_test.py +0 -0
  111. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/roadmap_test.py +0 -0
  112. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/scenarios.py +0 -0
  113. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/scenarios_test.py +0 -0
  114. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/suites.py +0 -0
  115. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/trial_test.py +0 -0
  116. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/integration_test.py +0 -0
  117. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/main.py +0 -0
  118. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/main_test.py +0 -0
  119. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/persistence.py +0 -0
  120. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/persistence_test.py +0 -0
  121. /lemming_cli-0.1.2/src/lemming/prompts/hooks/readability.md → /lemming_cli-0.1.4/src/lemming/prompts/hooks/50-readability.md +0 -0
  122. /lemming_cli-0.1.2/src/lemming/prompts/hooks/testing.md → /lemming_cli-0.1.4/src/lemming/prompts/hooks/60-testing.md +0 -0
  123. /lemming_cli-0.1.2/src/lemming/prompts/hooks/roadmap.md → /lemming_cli-0.1.4/src/lemming/prompts/hooks/90-roadmap.md +0 -0
  124. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/prompts/taskrunner.md +0 -0
  125. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/providers.py +0 -0
  126. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/providers_test.py +0 -0
  127. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/lifecycle_test.py +0 -0
  128. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/operations.py +0 -0
  129. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/operations_test.py +0 -0
  130. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/progress.py +0 -0
  131. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/progress_test.py +0 -0
  132. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/queries.py +0 -0
  133. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/queries_test.py +0 -0
  134. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/favicon.js +0 -0
  135. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/files.html +0 -0
  136. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/files.spec.js +0 -0
  137. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/logs.html +0 -0
  138. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/logs.test.js +0 -0
  139. {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/mancha.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lemming-cli
3
- Version: 0.1.2
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
@@ -158,19 +158,24 @@ tasks may not fully achieve the stated goal. **Orchestrator Hooks** address this
158
158
  by running custom agents or scripts after each task execution to evaluate
159
159
  results and adapt the roadmap.
160
160
 
161
- By default, Lemming runs all available hooks (including the built-in `roadmap`
162
- hook). You can customize this behavior via the `hooks` subcommand:
161
+ Lemming runs every hook it discovers on the filesystem (including the
162
+ built-in `roadmap` hook). Hooks are plain Markdown files, and udev-style
163
+ filename conventions control their behavior:
163
164
 
164
- ```bash
165
- # Enable or disable hooks for the project
166
- lemming hooks enable lint
167
- lemming hooks disable roadmap
165
+ - **Ordering**: A numeric prefix sets the execution order (e.g.
166
+ `10-lint.md` runs before `90-roadmap.md`); files without a prefix
167
+ default to priority 50.
168
+ - **Failure hooks**: Hooks at priority 90 and above also run when a task
169
+ fails; all others only run on success.
170
+ - **Disabling**: An empty file masks (disables) the hook of the same name
171
+ from a lower-precedence layer.
168
172
 
169
- # Set the exact list of active hooks
170
- lemming hooks set roadmap lint
173
+ ```bash
174
+ # Disable a hook for this project (writes an empty .lemming/hooks/50-lint.md)
175
+ lemming hooks disable lint
171
176
 
172
- # Reset to default (run all available hooks)
173
- lemming hooks reset
177
+ # Re-enable it (removes the mask file)
178
+ lemming hooks enable lint
174
179
  ```
175
180
 
176
181
  ### Built-in Hooks ⚓️
@@ -194,10 +199,10 @@ You can create your own hooks by adding Markdown files to:
194
199
  1. **Project-specific**: `.lemming/hooks/*.md`
195
200
  2. **Global**: `~/.local/lemming/hooks/*.md`
196
201
 
197
- Lemming's built-in hooks can be symlinked to the global directory using
198
- `lemming hooks install`. This allows you to easily **override** a built-in hook
199
- by replacing its symlink with a file, or **disable** a global override by
200
- deleting the symlink.
202
+ To **override** a built-in hook, create a file with the same logical name
203
+ (the numeric prefix is not part of the name, so `20-roadmap.md` overrides
204
+ the built-in `90-roadmap.md` and also moves it to priority 20); delete the
205
+ file to restore the built-in version.
201
206
 
202
207
  Hooks follow a specific discovery precedence: **Project > Global > Built-in**.
203
208
  See [docs/HOOKS.md](docs/HOOKS.md) for more details.
@@ -207,18 +212,14 @@ See [docs/HOOKS.md](docs/HOOKS.md) for more details.
207
212
  Use the `config` and `hooks` commands to manage your project's execution loop:
208
213
 
209
214
  ```bash
210
- # List all available hooks (built-in and local)
215
+ # List all hooks in execution order, with source and status
211
216
  lemming hooks list
212
217
 
213
- # Install (symlink) built-in hooks to the global directory
214
- lemming hooks install
215
-
216
- # View current project configuration
218
+ # View current project configuration (includes the active hooks)
217
219
  lemming config list
218
220
 
219
221
  # Persist configuration to tasks.yml
220
222
  lemming config set runner aider
221
- lemming hooks set roadmap lint
222
223
  ```
223
224
 
224
225
  ### Evaluating Prompt Changes
@@ -136,19 +136,24 @@ tasks may not fully achieve the stated goal. **Orchestrator Hooks** address this
136
136
  by running custom agents or scripts after each task execution to evaluate
137
137
  results and adapt the roadmap.
138
138
 
139
- By default, Lemming runs all available hooks (including the built-in `roadmap`
140
- hook). You can customize this behavior via the `hooks` subcommand:
139
+ Lemming runs every hook it discovers on the filesystem (including the
140
+ built-in `roadmap` hook). Hooks are plain Markdown files, and udev-style
141
+ filename conventions control their behavior:
142
+
143
+ - **Ordering**: A numeric prefix sets the execution order (e.g.
144
+ `10-lint.md` runs before `90-roadmap.md`); files without a prefix
145
+ default to priority 50.
146
+ - **Failure hooks**: Hooks at priority 90 and above also run when a task
147
+ fails; all others only run on success.
148
+ - **Disabling**: An empty file masks (disables) the hook of the same name
149
+ from a lower-precedence layer.
141
150
 
142
151
  ```bash
143
- # Enable or disable hooks for the project
144
- lemming hooks enable lint
145
- lemming hooks disable roadmap
146
-
147
- # Set the exact list of active hooks
148
- lemming hooks set roadmap lint
152
+ # Disable a hook for this project (writes an empty .lemming/hooks/50-lint.md)
153
+ lemming hooks disable lint
149
154
 
150
- # Reset to default (run all available hooks)
151
- lemming hooks reset
155
+ # Re-enable it (removes the mask file)
156
+ lemming hooks enable lint
152
157
  ```
153
158
 
154
159
  ### Built-in Hooks ⚓️
@@ -172,10 +177,10 @@ You can create your own hooks by adding Markdown files to:
172
177
  1. **Project-specific**: `.lemming/hooks/*.md`
173
178
  2. **Global**: `~/.local/lemming/hooks/*.md`
174
179
 
175
- Lemming's built-in hooks can be symlinked to the global directory using
176
- `lemming hooks install`. This allows you to easily **override** a built-in hook
177
- by replacing its symlink with a file, or **disable** a global override by
178
- deleting the symlink.
180
+ To **override** a built-in hook, create a file with the same logical name
181
+ (the numeric prefix is not part of the name, so `20-roadmap.md` overrides
182
+ the built-in `90-roadmap.md` and also moves it to priority 20); delete the
183
+ file to restore the built-in version.
179
184
 
180
185
  Hooks follow a specific discovery precedence: **Project > Global > Built-in**.
181
186
  See [docs/HOOKS.md](docs/HOOKS.md) for more details.
@@ -185,18 +190,14 @@ See [docs/HOOKS.md](docs/HOOKS.md) for more details.
185
190
  Use the `config` and `hooks` commands to manage your project's execution loop:
186
191
 
187
192
  ```bash
188
- # List all available hooks (built-in and local)
193
+ # List all hooks in execution order, with source and status
189
194
  lemming hooks list
190
195
 
191
- # Install (symlink) built-in hooks to the global directory
192
- lemming hooks install
193
-
194
- # View current project configuration
196
+ # View current project configuration (includes the active hooks)
195
197
  lemming config list
196
198
 
197
199
  # Persist configuration to tasks.yml
198
200
  lemming config set runner aider
199
- lemming hooks set roadmap lint
200
201
  ```
201
202
 
202
203
  ### Evaluating Prompt Changes
@@ -19,6 +19,27 @@ more **orchestrator hooks**. Each hook:
19
19
  execution log, and any commands it runs (e.g. `lemming add`, `lemming edit`)
20
20
  are executed against the roadmap.
21
21
 
22
+ ## Naming Convention
23
+
24
+ Hooks are Markdown files discovered from the filesystem, and udev-style
25
+ filename conventions control their behavior:
26
+
27
+ - **Ordering**: A numeric prefix determines execution order: `10-lint.md`
28
+ runs before `90-roadmap.md`. Files without a prefix default to priority
29
+ 50. The prefix is **not** part of the hook's name: `90-roadmap.md`
30
+ defines the hook `roadmap`.
31
+ - **Failure hooks**: Hooks at priority 90 and above also run when a task
32
+ fails. All other hooks only run after successful tasks. The built-in
33
+ `roadmap` hook ships as `90-roadmap.md` so it can react to failures.
34
+ - **Masking**: An empty file disables the hook of the same name from a
35
+ lower-precedence layer (see below). For example, an empty
36
+ `.lemming/hooks/readability.md` disables the built-in `readability` hook
37
+ for that project.
38
+ - **Overriding**: A non-empty file replaces the hook of the same name from
39
+ a lower-precedence layer. The winning filename also determines the
40
+ priority, so keep the `9x-` prefix when overriding a failure hook (e.g.
41
+ `90-roadmap.md`), or use a different prefix to deliberately reorder it.
42
+
22
43
  ## Built-in Hooks
23
44
 
24
45
  ### `roadmap` (Default)
@@ -57,35 +78,18 @@ locations:
57
78
 
58
79
  ### Precedence
59
80
 
60
- When Lemming looks for a hook template, it follows this order:
81
+ When Lemming looks for a hook, it resolves each logical name through the
82
+ following layers, highest precedence first:
61
83
 
62
84
  1. **Project-specific**: `.lemming/hooks/`
63
85
  2. **Global**: `~/.local/lemming/hooks/`
64
86
  3. **Built-in**: Bundled with the Lemming package.
65
87
 
66
- ### Global Hook Management
67
-
68
- Lemming's built-in hooks can be symlinked into the global directory
69
- (`~/.local/lemming/hooks/`) to make them easily discoverable and overridable.
70
-
71
- Use the following command to install the symlinks:
72
-
73
- ```bash
74
- lemming hooks install
75
- ```
76
-
77
- This provides several ways to manage them:
78
-
79
- - **Override**: Replace the symlink with your own Markdown file of the same
80
- name.
81
- - **Disable Global Override**: Delete the symlink from the global directory.
82
- Lemming will fall back to the built-in version unless the hook is also
83
- disabled in the project configuration.
84
- - **Restore Defaults**: Run `lemming hooks install`. Lemming will recreate any
85
- missing built-in symlinks.
86
-
87
- Users can also add their own global hooks to this directory, making them
88
- available to all Lemming projects on the system.
88
+ Because of this precedence order, overriding a built-in hook only requires
89
+ creating a Markdown file with the same logical name in the project or global
90
+ directory (e.g. `~/.local/lemming/hooks/90-roadmap.md`); delete the file to
91
+ restore the built-in version. Global hooks are available to all Lemming
92
+ projects on the system.
89
93
 
90
94
  ### Example: `lint` hook
91
95
 
@@ -107,29 +111,28 @@ are issues, add a new task to fix them.
107
111
 
108
112
  ## Using Hooks
109
113
 
110
- ### Configuration
114
+ ### Enabling and Disabling
111
115
 
112
- By default, Lemming runs all hooks configured in your `tasks.yml`. If no hooks
113
- are explicitly configured, all available hooks (built-in and project-specific)
114
- are executed.
116
+ Lemming runs every hook it discovers that is not masked. There is no hook
117
+ configuration in `tasks.yml`; the filesystem is the single source of truth.
115
118
 
116
- Use the `hooks` command group to manage which hooks should run automatically:
119
+ The `hooks` command group provides shortcuts for managing project masks:
117
120
 
118
121
  ```bash
119
- # Enable or disable one or more hooks
120
- lemming hooks enable lint
121
- lemming hooks disable roadmap lint
122
+ # Disable a hook for this project (writes an empty .lemming/hooks/50-lint.md)
123
+ lemming hooks disable lint
122
124
 
123
- # Set the exact list of active hooks
124
- lemming hooks set roadmap lint
125
-
126
- # Reset to run all available hooks (default)
127
- lemming hooks reset
125
+ # Re-enable it (removes the mask file)
126
+ lemming hooks enable lint
128
127
  ```
129
128
 
130
- Changes to hook configuration are picked up dynamically by the running
131
- orchestrator loop. You can toggle hooks from the CLI or the Web UI while Lemming
132
- is running, and the next task execution will respect the updated settings.
129
+ These commands validate the hook names before changing anything and refuse
130
+ to touch files with content (a non-empty file is an override, not a mask).
131
+ An empty file created by hand works just as well; the commands additionally
132
+ keep the hook's priority in the mask filename so listings stay accurate.
133
+
134
+ Hooks are re-discovered on every task execution, so changes are picked up
135
+ dynamically by a running orchestrator loop.
133
136
 
134
137
  ### Available Variables
135
138
 
@@ -146,7 +149,8 @@ Your hook template can use the following placeholders:
146
149
 
147
150
  ### Listing Hooks
148
151
 
149
- You can see all available hooks (built-in and local) using the CLI:
152
+ You can see all hooks in execution order, with their priority, source layer,
153
+ and status (disabled, runs on failure) using the CLI:
150
154
 
151
155
  ```bash
152
156
  lemming hooks list
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.1.2"
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 = [
@@ -0,0 +1,57 @@
1
+ """API routes for listing and toggling orchestrator hooks."""
2
+
3
+ import pathlib
4
+
5
+ import fastapi
6
+ import pydantic
7
+
8
+ from .. import hooks
9
+ from . import context
10
+
11
+ router = fastapi.APIRouter()
12
+
13
+
14
+ def _hook_payload(tasks_file: pathlib.Path) -> list[dict]:
15
+ """Serializes resolved hooks for API responses."""
16
+ return [
17
+ {
18
+ "name": h.name,
19
+ "priority": h.priority,
20
+ "source": h.source,
21
+ "masked": h.masked,
22
+ "runs_on_failure": h.priority >= hooks.FAILURE_HOOK_PRIORITY,
23
+ }
24
+ for h in hooks.resolve_hooks(tasks_file)
25
+ ]
26
+
27
+
28
+ @router.get("/api/hooks")
29
+ def list_hooks(request: fastapi.Request, project: str | None = None):
30
+ """List resolved orchestrator hooks in execution order."""
31
+ tasks_file = context.resolve_tasks_file(request.app.state, project)
32
+ return _hook_payload(tasks_file)
33
+
34
+
35
+ class HookToggle(pydantic.BaseModel):
36
+ """Request body for enabling or disabling a hook."""
37
+
38
+ name: str
39
+ enabled: bool
40
+
41
+
42
+ @router.post("/api/hooks")
43
+ def toggle_hook(
44
+ request: fastapi.Request,
45
+ toggle: HookToggle,
46
+ project: str | None = None,
47
+ ):
48
+ """Enable or disable a hook by removing or creating its project mask."""
49
+ tasks_file = context.resolve_tasks_file(request.app.state, project)
50
+ try:
51
+ if toggle.enabled:
52
+ hooks.enable_hooks([toggle.name], tasks_file)
53
+ else:
54
+ hooks.disable_hooks([toggle.name], tasks_file)
55
+ except ValueError as e:
56
+ raise fastapi.HTTPException(status_code=400, detail=str(e))
57
+ return _hook_payload(tasks_file)
@@ -0,0 +1,92 @@
1
+ import lemming.api
2
+
3
+
4
+ def test_list_hooks(client, test_tasks, monkeypatch, tmp_path):
5
+ """The /api/hooks endpoint returns resolved hooks in execution order."""
6
+ monkeypatch.setenv("LEMMING_HOME", str(tmp_path / "lemming_home"))
7
+
8
+ response = client.get("/api/hooks")
9
+
10
+ assert response.status_code == 200
11
+ payload = response.json()
12
+ hooks = {h["name"]: h for h in payload}
13
+ assert hooks["roadmap"]["priority"] == 90
14
+ assert hooks["roadmap"]["runs_on_failure"] is True
15
+ assert hooks["roadmap"]["masked"] is False
16
+ assert hooks["readability"]["source"] == "built-in"
17
+ assert hooks["readability"]["runs_on_failure"] is False
18
+
19
+ # Hooks are sorted by ascending priority
20
+ priorities = [h["priority"] for h in payload]
21
+ assert priorities == sorted(priorities)
22
+
23
+
24
+ def test_list_hooks_with_project(client, test_tasks, monkeypatch, tmp_path):
25
+ """The /api/hooks endpoint works with a specified project."""
26
+ monkeypatch.setenv("LEMMING_HOME", str(tmp_path / "lemming_home"))
27
+ root = lemming.api.app.state.root
28
+ project_dir = root / "project1"
29
+ project_dir.mkdir()
30
+ tasks_file = project_dir / "tasks.yml"
31
+ tasks_file.touch()
32
+
33
+ response = client.get("/api/hooks", params={"project": "project1"})
34
+
35
+ assert response.status_code == 200
36
+ assert "roadmap" in {h["name"] for h in response.json()}
37
+
38
+
39
+ def test_toggle_hook(client, test_tasks, monkeypatch, tmp_path):
40
+ """Disabling a hook creates a project mask; enabling removes it."""
41
+ monkeypatch.setenv("LEMMING_HOME", str(tmp_path / "lemming_home"))
42
+ # The mask filename keeps the hook's priority (roadmap is 90)
43
+ mask = test_tasks.parent / ".lemming" / "hooks" / "90-roadmap.md"
44
+
45
+ # Disable: the response reflects the mask and the file exists
46
+ response = client.post(
47
+ "/api/hooks", json={"name": "roadmap", "enabled": False}
48
+ )
49
+ assert response.status_code == 200
50
+ hooks = {h["name"]: h for h in response.json()}
51
+ assert hooks["roadmap"]["masked"] is True
52
+ assert mask.exists()
53
+ assert mask.read_text(encoding="utf-8") == ""
54
+
55
+ # Enable: the mask is removed
56
+ response = client.post(
57
+ "/api/hooks", json={"name": "roadmap", "enabled": True}
58
+ )
59
+ assert response.status_code == 200
60
+ hooks = {h["name"]: h for h in response.json()}
61
+ assert hooks["roadmap"]["masked"] is False
62
+ assert not mask.exists()
63
+
64
+
65
+ def test_toggle_unknown_hook(client, test_tasks, monkeypatch, tmp_path):
66
+ """Toggling an unknown hook returns a 400 error."""
67
+ monkeypatch.setenv("LEMMING_HOME", str(tmp_path / "lemming_home"))
68
+
69
+ response = client.post(
70
+ "/api/hooks", json={"name": "does-not-exist", "enabled": False}
71
+ )
72
+
73
+ assert response.status_code == 400
74
+ assert "not found" in response.json()["detail"]
75
+
76
+
77
+ def test_toggle_hook_refuses_project_override(
78
+ client, test_tasks, monkeypatch, tmp_path
79
+ ):
80
+ """Disabling a hook with a project override must not clobber it."""
81
+ monkeypatch.setenv("LEMMING_HOME", str(tmp_path / "lemming_home"))
82
+ local_hooks_dir = test_tasks.parent / ".lemming" / "hooks"
83
+ local_hooks_dir.mkdir(parents=True)
84
+ override = local_hooks_dir / "roadmap.md"
85
+ override.write_text("custom prompt", encoding="utf-8")
86
+
87
+ response = client.post(
88
+ "/api/hooks", json={"name": "roadmap", "enabled": False}
89
+ )
90
+
91
+ assert response.status_code == 400
92
+ assert override.read_text(encoding="utf-8") == "custom prompt"
@@ -2,7 +2,7 @@
2
2
 
3
3
  import click
4
4
 
5
- from .. import tasks
5
+ from .. import hooks, tasks
6
6
  from ..orchestrator import format_duration, parse_timeout
7
7
  from .main import cli
8
8
 
@@ -25,9 +25,12 @@ def config_list(ctx: click.Context):
25
25
  click.echo(f" Runner: {c.runner}")
26
26
  click.echo(f" Retries: {c.retries}")
27
27
  click.echo(f" Time limit: {format_duration(c.time_limit)}")
28
+
29
+ # Hooks are discovered from the filesystem, not stored in config
30
+ active_hooks = hooks.list_hooks(tasks_file)
28
31
  click.echo(
29
32
  " Hooks: "
30
- f"{', '.join(c.hooks) if c.hooks is not None else '(all)'}"
33
+ f"{', '.join(active_hooks) if active_hooks else '(none)'}"
31
34
  )
32
35
 
33
36
 
@@ -35,6 +35,9 @@ class TestCLIConfig(unittest.TestCase):
35
35
  )
36
36
  self.assertEqual(result.exit_code, 0)
37
37
  self.assertIn("Runner:", result.output)
38
+ # Active hooks are discovered from the filesystem and displayed
39
+ self.assertIn("Hooks:", result.output)
40
+ self.assertIn("roadmap", result.output)
38
41
 
39
42
  def test_config_set(self):
40
43
  result = self.cli_runner.invoke(
@@ -0,0 +1,85 @@
1
+ """CLI commands for managing orchestrator hooks."""
2
+
3
+ import click
4
+
5
+ from .. import hooks
6
+ from .main import cli
7
+
8
+
9
+ @cli.group(name="hooks", short_help="Manage orchestrator hooks")
10
+ def hooks_group():
11
+ """Manages orchestrator hooks.
12
+
13
+ Hooks are Markdown prompt files discovered from the project
14
+ (.lemming/hooks/), global (~/.local/lemming/hooks/), and built-in
15
+ layers, with the project layer taking precedence. A numeric filename
16
+ prefix (e.g. 90-roadmap.md) sets the execution order; hooks at 90 and
17
+ above also run when a task fails. An empty file disables (masks) the
18
+ hook of the same name.
19
+ """
20
+ pass
21
+
22
+
23
+ @hooks_group.command(name="list")
24
+ @click.pass_context
25
+ def hooks_list(ctx: click.Context):
26
+ """Lists hooks in execution order with source and status."""
27
+ tasks_file = ctx.obj["TASKS_FILE"]
28
+
29
+ click.secho("Orchestrator hooks (in execution order):", bold=True)
30
+ for hook in hooks.resolve_hooks(tasks_file):
31
+ notes = []
32
+ if hook.priority >= hooks.FAILURE_HOOK_PRIORITY:
33
+ notes.append("runs on failure")
34
+ if hook.masked:
35
+ notes.append("disabled")
36
+ suffix = f" ({', '.join(notes)})" if notes else ""
37
+ click.echo(
38
+ f" {hook.priority:>3} {hook.name:20} {hook.source}{suffix}"
39
+ )
40
+
41
+
42
+ @hooks_group.command(name="disable")
43
+ @click.argument("names", nargs=-1, required=True)
44
+ @click.pass_context
45
+ def hooks_disable(ctx: click.Context, names: tuple[str, ...]):
46
+ """Disables hooks for this project.
47
+
48
+ Writes an empty mask file to .lemming/hooks/, which shadows the hook of
49
+ the same name. An empty file created by hand works just as well; the
50
+ command additionally validates the names and keeps the hook's priority
51
+ in the mask filename so listings stay accurate.
52
+ """
53
+ tasks_file = ctx.obj["TASKS_FILE"]
54
+
55
+ try:
56
+ results = hooks.disable_hooks(list(names), tasks_file)
57
+ except ValueError as e:
58
+ click.echo(f"Error: {e}")
59
+ ctx.exit(1)
60
+
61
+ for name, mask in results.items():
62
+ if mask is None:
63
+ click.echo(f"Hook '{name}' is already disabled.")
64
+ else:
65
+ click.echo(f"Disabled hook: {name} (masked by {mask})")
66
+
67
+
68
+ @hooks_group.command(name="enable")
69
+ @click.argument("names", nargs=-1, required=True)
70
+ @click.pass_context
71
+ def hooks_enable(ctx: click.Context, names: tuple[str, ...]):
72
+ """Re-enables hooks by removing their project mask files."""
73
+ tasks_file = ctx.obj["TASKS_FILE"]
74
+
75
+ try:
76
+ results = hooks.enable_hooks(list(names), tasks_file)
77
+ except ValueError as e:
78
+ click.echo(f"Error: {e}")
79
+ ctx.exit(1)
80
+
81
+ for name, removed in results.items():
82
+ if removed:
83
+ click.echo(f"Enabled hook: {name}")
84
+ else:
85
+ click.echo(f"Hook '{name}' is already enabled.")