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.
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/PKG-INFO +22 -21
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/README.md +21 -20
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/docs/HOOKS.md +45 -41
- lemming_cli-0.1.4/docs/screenshots/dashboard-desktop.png +0 -0
- lemming_cli-0.1.4/docs/screenshots/dashboard-mobile.png +0 -0
- lemming_cli-0.1.4/docs/screenshots/task-log-desktop.png +0 -0
- lemming_cli-0.1.4/docs/screenshots/task-log-mobile.png +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/pyproject.toml +1 -1
- lemming_cli-0.1.4/src/lemming/api/hooks.py +57 -0
- lemming_cli-0.1.4/src/lemming/api/hooks_test.py +92 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/config.py +5 -2
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/config_test.py +3 -0
- lemming_cli-0.1.4/src/lemming/cli/hooks.py +85 -0
- lemming_cli-0.1.4/src/lemming/cli/hooks_test.py +123 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/readability.py +0 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/roadmap.py +1 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/trial.py +1 -1
- lemming_cli-0.1.4/src/lemming/hooks.py +238 -0
- lemming_cli-0.1.4/src/lemming/hooks_test.py +224 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/models.py +0 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/models_test.py +0 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/orchestrator.py +174 -4
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/orchestrator_test.py +317 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/paths.py +4 -2
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/paths_test.py +34 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/prompts.py +17 -105
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/prompts_test.py +20 -58
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/runner.py +5 -3
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/runner_test.py +5 -4
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/__init__.py +3 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/lifecycle.py +31 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/dashboard.spec.js +9 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/dashboard.test.js +0 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/favicon.spec.js +9 -1
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/index.html +5 -16
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/index.js +16 -15
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/screenshots.spec.js +23 -2
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/uv.lock +1 -1
- lemming_cli-0.1.2/docs/screenshots/dashboard-desktop.png +0 -0
- lemming_cli-0.1.2/docs/screenshots/dashboard-mobile.png +0 -0
- lemming_cli-0.1.2/docs/screenshots/task-log-desktop.png +0 -0
- lemming_cli-0.1.2/docs/screenshots/task-log-mobile.png +0 -0
- lemming_cli-0.1.2/src/lemming/api/hooks.py +0 -15
- lemming_cli-0.1.2/src/lemming/api/hooks_test.py +0 -33
- lemming_cli-0.1.2/src/lemming/cli/hooks.py +0 -147
- lemming_cli-0.1.2/src/lemming/cli/hooks_test.py +0 -50
- lemming_cli-0.1.2/src/lemming/hooks.py +0 -154
- lemming_cli-0.1.2/src/lemming/hooks_test.py +0 -227
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.github/workflows/ci.yml +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.github/workflows/publish.yml +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.github/workflows/screenshots.yml +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.gitignore +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.prettierignore +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/.prettierrc +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/Dockerfile +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/LICENSE +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/biome.json +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/docker-compose.yml +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/docs/EVALS.md +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/package-lock.json +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/package.json +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/playwright.config.js +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/__init__.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/__init__.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/auth.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/auth_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/config.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/config_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/conftest.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/context.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/context_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/directories.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/directories_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/files.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/files_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/logging.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/logging_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/loop.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/loop_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/main.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/main_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/tasks.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api/tasks_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/api_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/__init__.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/goal.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/goal_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/main.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/main_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/operations.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/operations_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/progress.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/progress_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/readability_cli.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/readability_cli_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/tasks.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli/tasks_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/cli_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/conftest.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/__init__.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/__main__.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/cli.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/cli_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/container.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/container_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/fixtures.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/fixtures_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/harness.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/harness_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/readability_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/roadmap_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/scenarios.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/scenarios_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/suites.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/evals/trial_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/integration_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/main.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/main_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/persistence.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/persistence_test.py +0 -0
- /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
- /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
- /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
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/prompts/taskrunner.md +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/providers.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/providers_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/lifecycle_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/operations.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/operations_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/progress.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/progress_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/queries.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/tasks/queries_test.py +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/favicon.js +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/files.html +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/files.spec.js +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/logs.html +0 -0
- {lemming_cli-0.1.2 → lemming_cli-0.1.4}/src/lemming/web/logs.test.js +0 -0
- {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.
|
|
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
|
-
|
|
162
|
-
hook).
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
#
|
|
173
|
-
lemming hooks
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
215
|
+
# List all hooks in execution order, with source and status
|
|
211
216
|
lemming hooks list
|
|
212
217
|
|
|
213
|
-
#
|
|
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
|
-
|
|
140
|
-
hook).
|
|
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
|
-
#
|
|
144
|
-
lemming hooks
|
|
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
|
-
#
|
|
151
|
-
lemming hooks
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
193
|
+
# List all hooks in execution order, with source and status
|
|
189
194
|
lemming hooks list
|
|
190
195
|
|
|
191
|
-
#
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
###
|
|
114
|
+
### Enabling and Disabling
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
119
|
+
The `hooks` command group provides shortcuts for managing project masks:
|
|
117
120
|
|
|
118
121
|
```bash
|
|
119
|
-
#
|
|
120
|
-
lemming hooks
|
|
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
|
-
#
|
|
124
|
-
lemming hooks
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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(
|
|
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.")
|