lemming-cli 0.2.0__tar.gz → 0.2.1__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.2.0 → lemming_cli-0.2.1}/PKG-INFO +14 -6
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/README.md +13 -5
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/HOOKS.md +19 -4
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/package-lock.json +4 -4
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/package.json +1 -1
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/pyproject.toml +1 -1
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/hooks_test.py +3 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/readability.py +134 -2
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/readability_test.py +80 -1
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/hooks_test.py +5 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/models.py +1 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/models_test.py +1 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/hooks/50-readability.md +20 -7
- lemming_cli-0.2.1/src/lemming/prompts/hooks/70-ux.md +55 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts_test.py +18 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/runner.py +36 -4
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/runner_test.py +53 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/__init__.py +3 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/lifecycle.py +36 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/lifecycle_test.py +20 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/dashboard.spec.js +62 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/dashboard.test.js +160 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/index.html +38 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/index.js +66 -0
- lemming_cli-0.2.1/src/lemming/web/mancha.js +58 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/screenshots.spec.js +0 -1
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/uv.lock +1 -1
- lemming_cli-0.2.0/src/lemming/web/mancha.js +0 -58
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.github/workflows/ci.yml +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.github/workflows/publish.yml +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.github/workflows/screenshots.yml +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.gitignore +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.prettierignore +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.prettierrc +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/Dockerfile +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/LICENSE +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/biome.json +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docker-compose.yml +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/EVALS.md +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/dashboard-desktop.png +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/dashboard-mobile.png +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/task-log-desktop.png +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/task-log-mobile.png +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/playwright.config.js +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/__init__.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/__init__.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/auth.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/auth_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/config.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/config_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/conftest.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/context.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/context_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/directories.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/directories_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/files.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/files_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/hooks.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/logging.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/logging_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/loop.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/loop_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/main.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/main_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/tasks.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/tasks_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/__init__.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/config.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/config_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/goal.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/goal_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/hooks.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/hooks_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/main.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/main_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/operations.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/operations_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/progress.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/progress_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/readability_cli.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/readability_cli_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/tasks.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/tasks_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/conftest.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/__init__.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/__main__.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/cli.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/cli_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/container.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/container_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/fixtures.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/fixtures_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/harness.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/harness_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/roadmap.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/roadmap_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/scenarios.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/scenarios_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/suites.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/trial.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/trial_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/hooks.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/integration_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/main.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/main_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/orchestrator.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/orchestrator_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/paths.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/paths_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/persistence.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/persistence_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/hooks/60-testing.md +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/taskrunner.md +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/providers.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/providers_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/operations.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/operations_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/progress.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/progress_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/queries.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/queries_test.py +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/favicon.js +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/favicon.spec.js +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/files.html +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/files.spec.js +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/logs.html +0 -0
- {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/logs.test.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lemming-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -186,11 +186,17 @@ Lemming comes with several built-in hooks to help manage your project:
|
|
|
186
186
|
analyzes the results of the finished task and decides if the remaining roadmap
|
|
187
187
|
needs to be adjusted (e.g., adding a missing prerequisite, skipping obsolete
|
|
188
188
|
tasks, or breaking down a broad task).
|
|
189
|
-
- **`readability`**: A code quality hook that
|
|
190
|
-
|
|
189
|
+
- **`readability`**: A code quality and simplification hook that challenges
|
|
190
|
+
unnecessary complexity and duplicate implementations, then reviews changes
|
|
191
|
+
for adherence to the Google Style Guide and general readability using the
|
|
191
192
|
[readability](https://github.com/owahltinez/readability) tool (exposed as
|
|
192
193
|
`lemming readability`). It can record findings as task progress or suggest
|
|
193
194
|
follow-up refactoring tasks.
|
|
195
|
+
- **`testing`**: Verifies that changed behavior has focused test coverage and
|
|
196
|
+
that the relevant tests pass.
|
|
197
|
+
- **`ux`**: Reviews at most one critical user journey affected by a
|
|
198
|
+
user-visible change. It reports only concrete, reproducible continuity gaps
|
|
199
|
+
and exits immediately for non-user-facing tasks.
|
|
194
200
|
|
|
195
201
|
### Custom and Global Hooks
|
|
196
202
|
|
|
@@ -307,10 +313,12 @@ Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
|
|
|
307
313
|
- **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions`
|
|
308
314
|
- **Aider**: Adds `--yes --quiet`
|
|
309
315
|
- **Claude**: Adds `--dangerously-skip-permissions`
|
|
310
|
-
- **Codex**:
|
|
316
|
+
- **Codex**: Runs non-interactively via `codex exec`, adds `--json`, and, in
|
|
317
|
+
YOLO mode, adds `--dangerously-bypass-approvals-and-sandbox`
|
|
311
318
|
|
|
312
|
-
You can disable
|
|
313
|
-
|
|
319
|
+
You can disable default flag injection with `--no-defaults` (`codex exec`
|
|
320
|
+
remains the Codex execution interface), or use a **template** to fully control
|
|
321
|
+
the command layout:
|
|
314
322
|
|
|
315
323
|
```bash
|
|
316
324
|
lemming run --runner "my-tool --input={{prompt}} --json"
|
|
@@ -164,11 +164,17 @@ Lemming comes with several built-in hooks to help manage your project:
|
|
|
164
164
|
analyzes the results of the finished task and decides if the remaining roadmap
|
|
165
165
|
needs to be adjusted (e.g., adding a missing prerequisite, skipping obsolete
|
|
166
166
|
tasks, or breaking down a broad task).
|
|
167
|
-
- **`readability`**: A code quality hook that
|
|
168
|
-
|
|
167
|
+
- **`readability`**: A code quality and simplification hook that challenges
|
|
168
|
+
unnecessary complexity and duplicate implementations, then reviews changes
|
|
169
|
+
for adherence to the Google Style Guide and general readability using the
|
|
169
170
|
[readability](https://github.com/owahltinez/readability) tool (exposed as
|
|
170
171
|
`lemming readability`). It can record findings as task progress or suggest
|
|
171
172
|
follow-up refactoring tasks.
|
|
173
|
+
- **`testing`**: Verifies that changed behavior has focused test coverage and
|
|
174
|
+
that the relevant tests pass.
|
|
175
|
+
- **`ux`**: Reviews at most one critical user journey affected by a
|
|
176
|
+
user-visible change. It reports only concrete, reproducible continuity gaps
|
|
177
|
+
and exits immediately for non-user-facing tasks.
|
|
172
178
|
|
|
173
179
|
### Custom and Global Hooks
|
|
174
180
|
|
|
@@ -285,10 +291,12 @@ Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
|
|
|
285
291
|
- **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions`
|
|
286
292
|
- **Aider**: Adds `--yes --quiet`
|
|
287
293
|
- **Claude**: Adds `--dangerously-skip-permissions`
|
|
288
|
-
- **Codex**:
|
|
294
|
+
- **Codex**: Runs non-interactively via `codex exec`, adds `--json`, and, in
|
|
295
|
+
YOLO mode, adds `--dangerously-bypass-approvals-and-sandbox`
|
|
289
296
|
|
|
290
|
-
You can disable
|
|
291
|
-
|
|
297
|
+
You can disable default flag injection with `--no-defaults` (`codex exec`
|
|
298
|
+
remains the Codex execution interface), or use a **template** to fully control
|
|
299
|
+
the command layout:
|
|
292
300
|
|
|
293
301
|
```bash
|
|
294
302
|
lemming run --runner "my-tool --input={{prompt}} --json"
|
|
@@ -51,15 +51,30 @@ a broad task).
|
|
|
51
51
|
|
|
52
52
|
### `readability`
|
|
53
53
|
|
|
54
|
-
A code quality hook that
|
|
55
|
-
and
|
|
56
|
-
|
|
54
|
+
A code quality and simplification hook that challenges unnecessary complexity,
|
|
55
|
+
indirection, and duplicated implementations before reviewing changes for
|
|
56
|
+
adherence to the Google Style Guide and general readability using the bundled
|
|
57
|
+
`lemming readability` tool. It provides feedback via task progress or suggests
|
|
58
|
+
follow-up refactoring tasks.
|
|
59
|
+
|
|
60
|
+
### `testing`
|
|
61
|
+
|
|
62
|
+
Checks that changed behavior has focused test coverage and runs the relevant
|
|
63
|
+
tests. It records larger testing gaps as task progress for the roadmap hook.
|
|
64
|
+
|
|
65
|
+
### `ux`
|
|
66
|
+
|
|
67
|
+
A lightweight journey-continuity review for user-visible changes. It considers
|
|
68
|
+
at most one affected critical journey, reports only concrete and reproducible
|
|
69
|
+
gaps, and exits immediately when the task did not change user-facing behavior.
|
|
70
|
+
It runs after `testing` and before `roadmap`.
|
|
57
71
|
|
|
58
72
|
### Editing Built-in Hook Prompts
|
|
59
73
|
|
|
60
74
|
The built-in hook prompts are load-bearing: a wording change can regress
|
|
61
75
|
orchestration behavior without any unit test failing. When you edit a prompt
|
|
62
|
-
under `src/lemming/prompts/hooks
|
|
76
|
+
under `src/lemming/prompts/hooks/` that has an eval suite, run it before
|
|
77
|
+
shipping:
|
|
63
78
|
|
|
64
79
|
```bash
|
|
65
80
|
uv run python -m lemming.evals run --suite roadmap
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@biomejs/biome": "1.9.4",
|
|
13
13
|
"@playwright/test": "^1.60.0",
|
|
14
14
|
"jsdom": "latest",
|
|
15
|
-
"mancha": "^0.22.
|
|
15
|
+
"mancha": "^0.22.7",
|
|
16
16
|
"playwright": "^1.60.0"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
@@ -1019,9 +1019,9 @@
|
|
|
1019
1019
|
}
|
|
1020
1020
|
},
|
|
1021
1021
|
"node_modules/mancha": {
|
|
1022
|
-
"version": "0.22.
|
|
1023
|
-
"resolved": "https://registry.npmjs.org/mancha/-/mancha-0.22.
|
|
1024
|
-
"integrity": "sha512-
|
|
1022
|
+
"version": "0.22.7",
|
|
1023
|
+
"resolved": "https://registry.npmjs.org/mancha/-/mancha-0.22.7.tgz",
|
|
1024
|
+
"integrity": "sha512-ZClxBTYOKSD9f4KqW8lCMYVEUPhxzIRgihw5g20QrD4kzZ9JcQ383LXXkPMVTaCAjLlqYaF3brUMYACwWgQZgA==",
|
|
1025
1025
|
"dev": true,
|
|
1026
1026
|
"license": "MIT",
|
|
1027
1027
|
"dependencies": {
|
|
@@ -15,6 +15,9 @@ def test_list_hooks(client, test_tasks, monkeypatch, tmp_path):
|
|
|
15
15
|
assert hooks["roadmap"]["masked"] is False
|
|
16
16
|
assert hooks["readability"]["source"] == "built-in"
|
|
17
17
|
assert hooks["readability"]["runs_on_failure"] is False
|
|
18
|
+
assert hooks["ux"]["priority"] == 70
|
|
19
|
+
assert hooks["ux"]["source"] == "built-in"
|
|
20
|
+
assert hooks["ux"]["runs_on_failure"] is False
|
|
18
21
|
|
|
19
22
|
# Hooks are sorted by ascending priority
|
|
20
23
|
priorities = [h["priority"] for h in payload]
|
|
@@ -56,6 +56,38 @@ def subtract(a: float, b: float) -> float:
|
|
|
56
56
|
return a - b
|
|
57
57
|
'''
|
|
58
58
|
|
|
59
|
+
_DUPLICATED_BEHAVIOR_OPS = (
|
|
60
|
+
'"""Arithmetic operations for the calculator CLI."""\n\n'
|
|
61
|
+
'''import math
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def add(a: float, b: float) -> float:
|
|
65
|
+
"""Returns the sum of two numbers."""
|
|
66
|
+
return a + b
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def subtract(a: float, b: float) -> float:
|
|
70
|
+
"""Returns the difference of two numbers."""
|
|
71
|
+
return a - b
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def add_for_receipt(a: float, b: float) -> float:
|
|
75
|
+
"""Returns a validated, receipt-ready sum."""
|
|
76
|
+
total = a + b
|
|
77
|
+
if not math.isfinite(total):
|
|
78
|
+
raise ValueError("Receipt totals must be finite.")
|
|
79
|
+
return round(total, 2)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def subtract_for_receipt(a: float, b: float) -> float:
|
|
83
|
+
"""Returns a validated, receipt-ready difference."""
|
|
84
|
+
total = a - b
|
|
85
|
+
if not math.isfinite(total):
|
|
86
|
+
raise ValueError("Receipt totals must be finite.")
|
|
87
|
+
return round(total, 2)
|
|
88
|
+
'''
|
|
89
|
+
)
|
|
90
|
+
|
|
59
91
|
_OPS_TEST = """import unittest
|
|
60
92
|
|
|
61
93
|
from calc import ops
|
|
@@ -69,6 +101,31 @@ class TestOps(unittest.TestCase):
|
|
|
69
101
|
self.assertEqual(ops.subtract(5, 3), 2)
|
|
70
102
|
"""
|
|
71
103
|
|
|
104
|
+
_DUPLICATED_BEHAVIOR_TEST = """import unittest
|
|
105
|
+
|
|
106
|
+
from calc import ops
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestOps(unittest.TestCase):
|
|
110
|
+
def test_add(self):
|
|
111
|
+
self.assertEqual(ops.add(2, 3), 5)
|
|
112
|
+
|
|
113
|
+
def test_subtract(self):
|
|
114
|
+
self.assertEqual(ops.subtract(5, 3), 2)
|
|
115
|
+
|
|
116
|
+
def test_add_for_receipt(self):
|
|
117
|
+
self.assertEqual(ops.add_for_receipt(2.126, 1), 3.13)
|
|
118
|
+
|
|
119
|
+
def test_subtract_for_receipt(self):
|
|
120
|
+
self.assertEqual(ops.subtract_for_receipt(5.126, 2), 3.13)
|
|
121
|
+
|
|
122
|
+
def test_receipt_totals_must_be_finite(self):
|
|
123
|
+
with self.assertRaises(ValueError):
|
|
124
|
+
ops.add_for_receipt(float("inf"), 1)
|
|
125
|
+
with self.assertRaises(ValueError):
|
|
126
|
+
ops.subtract_for_receipt(float("inf"), 1)
|
|
127
|
+
"""
|
|
128
|
+
|
|
72
129
|
# A messy module the finished task did NOT touch: tempting to clean up,
|
|
73
130
|
# but strictly out of scope for the hook.
|
|
74
131
|
_MESSY_LEGACY = '''"""Legacy report formatting kept for compatibility."""
|
|
@@ -104,14 +161,18 @@ _BASE_PROGRESS = [
|
|
|
104
161
|
]
|
|
105
162
|
|
|
106
163
|
|
|
107
|
-
def _write_project(
|
|
164
|
+
def _write_project(
|
|
165
|
+
workspace: pathlib.Path,
|
|
166
|
+
ops_source: str,
|
|
167
|
+
test_source: str = _OPS_TEST,
|
|
168
|
+
) -> None:
|
|
108
169
|
"""Seeds the fixture project with the given ops module."""
|
|
109
170
|
fixtures.init_repo(
|
|
110
171
|
workspace,
|
|
111
172
|
{
|
|
112
173
|
"calc/__init__.py": "",
|
|
113
174
|
"calc/ops.py": ops_source,
|
|
114
|
-
"calc/ops_test.py":
|
|
175
|
+
"calc/ops_test.py": test_source,
|
|
115
176
|
"calc/legacy.py": _MESSY_LEGACY,
|
|
116
177
|
"README.md": "# Calculator CLI\n",
|
|
117
178
|
},
|
|
@@ -253,6 +314,66 @@ def _grade_dead_code(workspace: pathlib.Path) -> list[scenarios.Check]:
|
|
|
253
314
|
return [*checks, acted, interface]
|
|
254
315
|
|
|
255
316
|
|
|
317
|
+
def _build_duplicated_behavior(workspace: pathlib.Path) -> None:
|
|
318
|
+
"""Fixture: two live functions duplicate receipt preparation."""
|
|
319
|
+
_write_project(
|
|
320
|
+
workspace,
|
|
321
|
+
_DUPLICATED_BEHAVIOR_OPS,
|
|
322
|
+
_DUPLICATED_BEHAVIOR_TEST,
|
|
323
|
+
)
|
|
324
|
+
_save_finished_task(
|
|
325
|
+
workspace,
|
|
326
|
+
[
|
|
327
|
+
"Modified calc/ops.py and calc/ops_test.py to add receipt-ready "
|
|
328
|
+
"addition and subtraction; all tests pass.",
|
|
329
|
+
],
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _grade_duplicated_behavior(
|
|
334
|
+
workspace: pathlib.Path,
|
|
335
|
+
) -> list[scenarios.Check]:
|
|
336
|
+
"""The hook must consolidate or report live near-duplicate behavior."""
|
|
337
|
+
checks = _common_checks(workspace)
|
|
338
|
+
|
|
339
|
+
roadmap, load_failures = scenarios.load_or_fail(workspace)
|
|
340
|
+
if roadmap is None:
|
|
341
|
+
return load_failures
|
|
342
|
+
|
|
343
|
+
source = (workspace / "calc" / "ops.py").read_text()
|
|
344
|
+
repeated_validation = source.count("math.isfinite(total)") > 1
|
|
345
|
+
repeated_rounding = source.count("round(total, 2)") > 1
|
|
346
|
+
consolidated = not repeated_validation and not repeated_rounding
|
|
347
|
+
|
|
348
|
+
task = next(t for t in roadmap.tasks if t.id == "task1")
|
|
349
|
+
new_entries = task.progress[1:]
|
|
350
|
+
reported = any(
|
|
351
|
+
"duplicat" in entry.lower()
|
|
352
|
+
and ("add_for_receipt" in entry or "subtract_for_receipt" in entry)
|
|
353
|
+
for entry in new_entries
|
|
354
|
+
)
|
|
355
|
+
acted = scenarios.Check(
|
|
356
|
+
name="acted-on-live-duplication",
|
|
357
|
+
passed=consolidated or reported,
|
|
358
|
+
detail=(
|
|
359
|
+
"live receipt logic stayed duplicated and no finding was recorded"
|
|
360
|
+
)
|
|
361
|
+
if not (consolidated or reported)
|
|
362
|
+
else "",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
interface = scenarios.Check(
|
|
366
|
+
name="receipt-interface-preserved",
|
|
367
|
+
passed=(
|
|
368
|
+
"def add_for_receipt(" in source
|
|
369
|
+
and "def subtract_for_receipt(" in source
|
|
370
|
+
),
|
|
371
|
+
detail="a public receipt operation disappeared",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
return [*checks, acted, interface]
|
|
375
|
+
|
|
376
|
+
|
|
256
377
|
def _build_scope_limit(workspace: pathlib.Path) -> None:
|
|
257
378
|
"""Fixture: an untouched legacy file is messy but out of scope."""
|
|
258
379
|
_write_project(workspace, _CLEAN_OPS)
|
|
@@ -302,6 +423,17 @@ SCENARIOS = [
|
|
|
302
423
|
build=_build_dead_code,
|
|
303
424
|
grade=_grade_dead_code,
|
|
304
425
|
),
|
|
426
|
+
scenarios.Scenario(
|
|
427
|
+
name="consolidate-or-report-live-duplication",
|
|
428
|
+
hook="readability",
|
|
429
|
+
outcome=models.TaskStatus.COMPLETED,
|
|
430
|
+
task_id="task1",
|
|
431
|
+
summary=(
|
|
432
|
+
"Consolidates near-duplicate live behavior or records a finding."
|
|
433
|
+
),
|
|
434
|
+
build=_build_duplicated_behavior,
|
|
435
|
+
grade=_grade_duplicated_behavior,
|
|
436
|
+
),
|
|
305
437
|
scenarios.Scenario(
|
|
306
438
|
name="scope-limited-to-changed-files",
|
|
307
439
|
hook="readability",
|
|
@@ -38,7 +38,7 @@ class TestSuiteRegistry(ScenarioTestCase):
|
|
|
38
38
|
self.assertIn("readability", registry)
|
|
39
39
|
names = [s.name for s in registry["readability"]]
|
|
40
40
|
self.assertEqual(len(names), len(set(names)))
|
|
41
|
-
self.assertEqual(len(names),
|
|
41
|
+
self.assertEqual(len(names), 5)
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class TestFastExitScenario(ScenarioTestCase):
|
|
@@ -117,6 +117,85 @@ class TestDeadCodeScenario(ScenarioTestCase):
|
|
|
117
117
|
self.assertIn("interface-preserved", self.failed_names(checks))
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
class TestDuplicatedBehaviorScenario(ScenarioTestCase):
|
|
121
|
+
def test_ignoring_live_duplication_fails(self):
|
|
122
|
+
self.build("consolidate-or-report-live-duplication")
|
|
123
|
+
|
|
124
|
+
checks = self.scenario.grade(self.workspace)
|
|
125
|
+
self.assertEqual(
|
|
126
|
+
self.failed_names(checks),
|
|
127
|
+
{"acted-on-live-duplication"},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def test_consolidating_live_duplication_passes(self):
|
|
131
|
+
self.build("consolidate-or-report-live-duplication")
|
|
132
|
+
ops = self.workspace / "calc" / "ops.py"
|
|
133
|
+
source = ops.read_text()
|
|
134
|
+
source = source.replace(
|
|
135
|
+
'''def add_for_receipt(a: float, b: float) -> float:
|
|
136
|
+
"""Returns a validated, receipt-ready sum."""
|
|
137
|
+
total = a + b
|
|
138
|
+
if not math.isfinite(total):
|
|
139
|
+
raise ValueError("Receipt totals must be finite.")
|
|
140
|
+
return round(total, 2)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def subtract_for_receipt(a: float, b: float) -> float:
|
|
144
|
+
"""Returns a validated, receipt-ready difference."""
|
|
145
|
+
total = a - b
|
|
146
|
+
if not math.isfinite(total):
|
|
147
|
+
raise ValueError("Receipt totals must be finite.")
|
|
148
|
+
return round(total, 2)
|
|
149
|
+
''',
|
|
150
|
+
'''def _for_receipt(total: float) -> float:
|
|
151
|
+
"""Validates and rounds a receipt total."""
|
|
152
|
+
if not math.isfinite(total):
|
|
153
|
+
raise ValueError("Receipt totals must be finite.")
|
|
154
|
+
return round(total, 2)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def add_for_receipt(a: float, b: float) -> float:
|
|
158
|
+
"""Returns a validated, receipt-ready sum."""
|
|
159
|
+
return _for_receipt(a + b)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def subtract_for_receipt(a: float, b: float) -> float:
|
|
163
|
+
"""Returns a validated, receipt-ready difference."""
|
|
164
|
+
return _for_receipt(a - b)
|
|
165
|
+
''',
|
|
166
|
+
)
|
|
167
|
+
ops.write_text(source)
|
|
168
|
+
|
|
169
|
+
checks = self.scenario.grade(self.workspace)
|
|
170
|
+
self.assertEqual(self.failed_names(checks), set())
|
|
171
|
+
|
|
172
|
+
def test_recording_live_duplication_passes(self):
|
|
173
|
+
self.build("consolidate-or-report-live-duplication")
|
|
174
|
+
tasks.add_progress(
|
|
175
|
+
self.tasks_file,
|
|
176
|
+
"task1",
|
|
177
|
+
"Readability: add_for_receipt() and subtract_for_receipt() "
|
|
178
|
+
"duplicate receipt validation and rounding; consolidate them "
|
|
179
|
+
"behind one helper.",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
checks = self.scenario.grade(self.workspace)
|
|
183
|
+
self.assertEqual(self.failed_names(checks), set())
|
|
184
|
+
|
|
185
|
+
def test_deleting_receipt_operation_fails(self):
|
|
186
|
+
self.build("consolidate-or-report-live-duplication")
|
|
187
|
+
ops = self.workspace / "calc" / "ops.py"
|
|
188
|
+
source = ops.read_text()
|
|
189
|
+
start = source.index("def subtract_for_receipt")
|
|
190
|
+
ops.write_text(source[:start])
|
|
191
|
+
|
|
192
|
+
checks = self.scenario.grade(self.workspace)
|
|
193
|
+
self.assertIn(
|
|
194
|
+
"receipt-interface-preserved",
|
|
195
|
+
self.failed_names(checks),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
120
199
|
class TestScopeLimitScenario(ScenarioTestCase):
|
|
121
200
|
def test_leaving_legacy_alone_passes(self):
|
|
122
201
|
self.build("scope-limited-to-changed-files")
|
|
@@ -30,6 +30,8 @@ def test_list_hooks_includes_all(tmp_path, monkeypatch):
|
|
|
30
30
|
|
|
31
31
|
assert "roadmap" in active
|
|
32
32
|
assert "readability" in active
|
|
33
|
+
assert "testing" in active
|
|
34
|
+
assert "ux" in active
|
|
33
35
|
assert "custom_hook" in active
|
|
34
36
|
|
|
35
37
|
|
|
@@ -47,6 +49,9 @@ def test_list_hooks_orders_by_priority(tmp_path, monkeypatch):
|
|
|
47
49
|
# Numeric prefix orders execution; built-in roadmap (90) runs last
|
|
48
50
|
assert active[0] == "early"
|
|
49
51
|
assert active[-1] == "roadmap"
|
|
52
|
+
assert active.index("readability") < active.index("testing")
|
|
53
|
+
assert active.index("testing") < active.index("ux")
|
|
54
|
+
assert active.index("ux") < active.index("roadmap")
|
|
50
55
|
assert active.index("zz_hook") < active.index("roadmap")
|
|
51
56
|
|
|
52
57
|
|
|
@@ -56,6 +56,7 @@ class Task(pydantic.BaseModel):
|
|
|
56
56
|
last_started_at: float | None = None
|
|
57
57
|
created_at: float = pydantic.Field(default_factory=time.time)
|
|
58
58
|
run_time: float = 0.0
|
|
59
|
+
execution_times: dict[str, float] | None = None
|
|
59
60
|
pid: int | None = None
|
|
60
61
|
last_heartbeat: float | None = None
|
|
61
62
|
has_runner_log: bool = False
|
|
@@ -18,7 +18,20 @@ keeps quality from drifting as tasks accumulate.
|
|
|
18
18
|
1. **Automate**: Immediately run `lemming readability check <path> --fix` for
|
|
19
19
|
every file modified or created in the last task. This handles standard
|
|
20
20
|
formatting (ruff, biome, prettier) and type checking (pyrefly).
|
|
21
|
-
2. **
|
|
21
|
+
2. **Challenge the Shape**: Before polishing the implementation, ask whether
|
|
22
|
+
the same behavior could use fewer concepts, files, branches, dependencies,
|
|
23
|
+
or abstractions. Prefer removing indirection over adding another layer.
|
|
24
|
+
In particular, look for:
|
|
25
|
+
- **Duplication and drift**: parallel implementations of the same behavior
|
|
26
|
+
or knowledge with only small variations. Reuse or consolidate an existing
|
|
27
|
+
implementation and parameterize genuine differences so there is one
|
|
28
|
+
source of truth.
|
|
29
|
+
- **False reuse**: code that merely looks similar but represents different
|
|
30
|
+
concepts. Do not force one-off similarities into a premature abstraction
|
|
31
|
+
just to satisfy DRY.
|
|
32
|
+
Make only local, behavior-preserving simplifications within the files
|
|
33
|
+
changed by the task. Record broader redesigns as progress.
|
|
34
|
+
3. **Review**: Read the changed files and look for quality drift that automated
|
|
22
35
|
tools cannot catch:
|
|
23
36
|
- **Excess complexity**: deep nesting, sprawling functions, needless
|
|
24
37
|
indirection or premature abstraction, dead or duplicated code.
|
|
@@ -26,24 +39,24 @@ keeps quality from drifting as tasks accumulate.
|
|
|
26
39
|
developer of the language would not write.
|
|
27
40
|
- **Inconsistency**: code that diverges from the conventions of the
|
|
28
41
|
surrounding codebase, or comments that no longer match the code.
|
|
29
|
-
|
|
42
|
+
4. **Consult**: Fetch the style guide with
|
|
30
43
|
`lemming readability guide <language>` when reviewing and cite the relevant
|
|
31
44
|
rule in your findings. Do not rely on memory for style rules.
|
|
32
|
-
|
|
45
|
+
5. **Fix**: Apply targeted, behavior-preserving fixes for the issues you find.
|
|
33
46
|
Keep each fix small and scoped to the files changed in the last task. Do not
|
|
34
47
|
change public interfaces, feature behavior, or unrelated files; record those
|
|
35
48
|
findings as progress instead of fixing them.
|
|
36
|
-
|
|
49
|
+
6. **Verify**: After any manual fix, run the relevant tests and re-run
|
|
37
50
|
`lemming readability check <path>`. If verification fails and the fix is not
|
|
38
51
|
trivially repaired, revert your edits and record the finding as progress
|
|
39
52
|
rather than letting changes snowball.
|
|
40
|
-
|
|
53
|
+
7. **Report**: Record meaningful findings and applied fixes as progress using
|
|
41
54
|
`lemming progress {{finished_task_id}} '<finding>'`.
|
|
42
|
-
|
|
55
|
+
8. **No Orchestration**: Do NOT add new tasks to the roadmap. If you identify
|
|
43
56
|
significant issues that require follow-up work (e.g. a refactor spanning
|
|
44
57
|
unrelated files), record them as progress so the roadmap hook can decide
|
|
45
58
|
whether to add a new task.
|
|
46
|
-
|
|
59
|
+
9. **Fast Exit**: If the automated checks pass and your review finds no drift,
|
|
47
60
|
exit immediately.
|
|
48
61
|
|
|
49
62
|
## Commands
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# UX Journey Hook
|
|
2
|
+
|
|
3
|
+
You are a lightweight reviewer of the user journey affected by the task that
|
|
4
|
+
just finished. Your role is to catch concrete interaction dead ends that code
|
|
5
|
+
quality and unit-test review can miss, without turning each task into a general
|
|
6
|
+
UX audit.
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
{{roadmap}}
|
|
11
|
+
|
|
12
|
+
## Finished Task
|
|
13
|
+
|
|
14
|
+
{{finished_task}}
|
|
15
|
+
|
|
16
|
+
## Directives
|
|
17
|
+
|
|
18
|
+
1. **Applicability Gate**: First decide whether the finished task changed
|
|
19
|
+
user-visible behavior in a UI, CLI, API, workflow, documentation path, or
|
|
20
|
+
user-facing error. If it did not, exit immediately without running commands
|
|
21
|
+
or recording progress.
|
|
22
|
+
2. **Use the Contract**: Read applicable project instructions and existing
|
|
23
|
+
UX or journey documentation, if present. Otherwise use the task acceptance
|
|
24
|
+
criteria and observed user-facing behavior as the contract. Do not invent
|
|
25
|
+
personas, product requirements, aesthetic preferences, or new journey
|
|
26
|
+
documentation.
|
|
27
|
+
3. **Review One Journey**: Identify at most one critical journey directly
|
|
28
|
+
affected by this task and check only:
|
|
29
|
+
- Can the user discover and start it?
|
|
30
|
+
- Can the user complete its happy path within any documented step budget?
|
|
31
|
+
- Are failures understandable and recoverable without losing important
|
|
32
|
+
state?
|
|
33
|
+
- Is successful completion communicated accessibly and unambiguously?
|
|
34
|
+
4. **Require Evidence**: Report only a concrete, reproducible gap tied to a
|
|
35
|
+
requirement, changed behavior, failing assertion, or missing state
|
|
36
|
+
transition. Never judge a screenshot or self-certify subjective visual
|
|
37
|
+
quality. Prefer the project's existing focused checks and measurements.
|
|
38
|
+
5. **Stay Advisory**: Do NOT modify source files, tests, documentation, or the
|
|
39
|
+
roadmap. Do NOT rerun broad test suites already covered by the testing hook.
|
|
40
|
+
Record findings as progress so the roadmap hook can decide whether
|
|
41
|
+
follow-up work is necessary.
|
|
42
|
+
6. **Limit Output**: Record at most two findings. Each finding must name the
|
|
43
|
+
affected journey, the exact break or risk, and the evidence needed to close
|
|
44
|
+
it. If there is no concrete gap, exit immediately without recording a
|
|
45
|
+
generic approval or summary.
|
|
46
|
+
|
|
47
|
+
## Command
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
lemming --tasks-file {{tasks_file_path}} progress {{finished_task_id}} '<finding>'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Limit the review to behavior affected by the finished task. This is a journey
|
|
54
|
+
continuity check, not a design review, accessibility audit,
|
|
55
|
+
feature-completeness review, or request for polish.
|
|
@@ -9,6 +9,24 @@ def test_load_prompt():
|
|
|
9
9
|
assert "description" in prompt
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def test_load_builtin_ux_prompt():
|
|
13
|
+
prompt = prompts.load_prompt("ux")
|
|
14
|
+
|
|
15
|
+
assert "Applicability Gate" in prompt
|
|
16
|
+
assert "Review One Journey" in prompt
|
|
17
|
+
assert "{{finished_task}}" in prompt
|
|
18
|
+
assert "{{finished_task_id}}" in prompt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_load_builtin_readability_prompt_includes_simplification():
|
|
22
|
+
prompt = prompts.load_prompt("readability")
|
|
23
|
+
|
|
24
|
+
assert "Challenge the Shape" in prompt
|
|
25
|
+
assert "Duplication and drift" in prompt
|
|
26
|
+
assert "source of truth" in prompt
|
|
27
|
+
assert "False reuse" in prompt
|
|
28
|
+
|
|
29
|
+
|
|
12
30
|
def test_prepare_prompt(tmp_path):
|
|
13
31
|
tasks_file = tmp_path / "tasks.yml"
|
|
14
32
|
data = tasks.Roadmap(
|