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.
Files changed (131) hide show
  1. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/PKG-INFO +14 -6
  2. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/README.md +13 -5
  3. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/HOOKS.md +19 -4
  4. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/package-lock.json +4 -4
  5. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/package.json +1 -1
  6. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/pyproject.toml +1 -1
  7. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/hooks_test.py +3 -0
  8. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/readability.py +134 -2
  9. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/readability_test.py +80 -1
  10. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/hooks_test.py +5 -0
  11. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/models.py +1 -0
  12. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/models_test.py +1 -0
  13. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/hooks/50-readability.md +20 -7
  14. lemming_cli-0.2.1/src/lemming/prompts/hooks/70-ux.md +55 -0
  15. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts_test.py +18 -0
  16. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/runner.py +36 -4
  17. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/runner_test.py +53 -0
  18. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/__init__.py +3 -0
  19. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/lifecycle.py +36 -0
  20. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/lifecycle_test.py +20 -0
  21. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/dashboard.spec.js +62 -0
  22. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/dashboard.test.js +160 -0
  23. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/index.html +38 -0
  24. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/index.js +66 -0
  25. lemming_cli-0.2.1/src/lemming/web/mancha.js +58 -0
  26. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/screenshots.spec.js +0 -1
  27. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/uv.lock +1 -1
  28. lemming_cli-0.2.0/src/lemming/web/mancha.js +0 -58
  29. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.github/workflows/ci.yml +0 -0
  30. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.github/workflows/publish.yml +0 -0
  31. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.github/workflows/screenshots.yml +0 -0
  32. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.gitignore +0 -0
  33. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.prettierignore +0 -0
  34. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/.prettierrc +0 -0
  35. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/Dockerfile +0 -0
  36. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/LICENSE +0 -0
  37. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/biome.json +0 -0
  38. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docker-compose.yml +0 -0
  39. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/EVALS.md +0 -0
  40. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/dashboard-desktop.png +0 -0
  41. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/dashboard-mobile.png +0 -0
  42. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/task-log-desktop.png +0 -0
  43. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/docs/screenshots/task-log-mobile.png +0 -0
  44. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/playwright.config.js +0 -0
  45. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/__init__.py +0 -0
  46. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/__init__.py +0 -0
  47. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/auth.py +0 -0
  48. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/auth_test.py +0 -0
  49. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/config.py +0 -0
  50. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/config_test.py +0 -0
  51. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/conftest.py +0 -0
  52. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/context.py +0 -0
  53. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/context_test.py +0 -0
  54. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/directories.py +0 -0
  55. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/directories_test.py +0 -0
  56. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/files.py +0 -0
  57. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/files_test.py +0 -0
  58. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/hooks.py +0 -0
  59. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/logging.py +0 -0
  60. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/logging_test.py +0 -0
  61. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/loop.py +0 -0
  62. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/loop_test.py +0 -0
  63. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/main.py +0 -0
  64. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/main_test.py +0 -0
  65. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/tasks.py +0 -0
  66. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api/tasks_test.py +0 -0
  67. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/api_test.py +0 -0
  68. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/__init__.py +0 -0
  69. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/config.py +0 -0
  70. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/config_test.py +0 -0
  71. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/goal.py +0 -0
  72. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/goal_test.py +0 -0
  73. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/hooks.py +0 -0
  74. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/hooks_test.py +0 -0
  75. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/main.py +0 -0
  76. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/main_test.py +0 -0
  77. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/operations.py +0 -0
  78. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/operations_test.py +0 -0
  79. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/progress.py +0 -0
  80. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/progress_test.py +0 -0
  81. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/readability_cli.py +0 -0
  82. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/readability_cli_test.py +0 -0
  83. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/tasks.py +0 -0
  84. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli/tasks_test.py +0 -0
  85. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/cli_test.py +0 -0
  86. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/conftest.py +0 -0
  87. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/__init__.py +0 -0
  88. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/__main__.py +0 -0
  89. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/cli.py +0 -0
  90. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/cli_test.py +0 -0
  91. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/container.py +0 -0
  92. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/container_test.py +0 -0
  93. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/fixtures.py +0 -0
  94. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/fixtures_test.py +0 -0
  95. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/harness.py +0 -0
  96. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/harness_test.py +0 -0
  97. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/roadmap.py +0 -0
  98. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/roadmap_test.py +0 -0
  99. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/scenarios.py +0 -0
  100. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/scenarios_test.py +0 -0
  101. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/suites.py +0 -0
  102. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/trial.py +0 -0
  103. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/evals/trial_test.py +0 -0
  104. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/hooks.py +0 -0
  105. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/integration_test.py +0 -0
  106. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/main.py +0 -0
  107. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/main_test.py +0 -0
  108. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/orchestrator.py +0 -0
  109. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/orchestrator_test.py +0 -0
  110. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/paths.py +0 -0
  111. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/paths_test.py +0 -0
  112. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/persistence.py +0 -0
  113. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/persistence_test.py +0 -0
  114. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/hooks/60-testing.md +0 -0
  115. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
  116. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts/taskrunner.md +0 -0
  117. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/prompts.py +0 -0
  118. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/providers.py +0 -0
  119. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/providers_test.py +0 -0
  120. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/operations.py +0 -0
  121. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/operations_test.py +0 -0
  122. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/progress.py +0 -0
  123. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/progress_test.py +0 -0
  124. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/queries.py +0 -0
  125. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/tasks/queries_test.py +0 -0
  126. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/favicon.js +0 -0
  127. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/favicon.spec.js +0 -0
  128. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/files.html +0 -0
  129. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/files.spec.js +0 -0
  130. {lemming_cli-0.2.0 → lemming_cli-0.2.1}/src/lemming/web/logs.html +0 -0
  131. {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.0
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 reviews changes for adherence to
190
- the Google Style Guide and general readability using the
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**: Adds `--yolo`
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 this behavior with `--no-defaults`, or use a **template** to
313
- fully control the command layout:
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 reviews changes for adherence to
168
- the Google Style Guide and general readability using the
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**: Adds `--yolo`
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 this behavior with `--no-defaults`, or use a **template** to
291
- fully control the command layout:
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 reviews changes for adherence to the Google Style Guide
55
- and general readability using the bundled `lemming readability` tool. It
56
- provides feedback via task progress or suggests follow-up refactoring tasks.
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/`, run its eval suite before shipping:
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.4",
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.4",
1023
- "resolved": "https://registry.npmjs.org/mancha/-/mancha-0.22.4.tgz",
1024
- "integrity": "sha512-bNurE3cVS3ZoOow3hhkJiJcxRkXCZrWh4NaVVlV1KYFe3iY3wrcAqk/HJKMHw0kglcQjUc2iAtJbemI2oJKIUg==",
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": {
@@ -16,7 +16,7 @@
16
16
  "@biomejs/biome": "1.9.4",
17
17
  "@playwright/test": "^1.60.0",
18
18
  "jsdom": "latest",
19
- "mancha": "^0.22.4",
19
+ "mancha": "^0.22.7",
20
20
  "playwright": "^1.60.0"
21
21
  }
22
22
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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(workspace: pathlib.Path, ops_source: str) -> None:
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": _OPS_TEST,
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), 4)
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
@@ -17,6 +17,7 @@ def test_task_model_defaults():
17
17
  assert task.attempts == 0
18
18
  assert task.progress == []
19
19
  assert task.run_time == 0.0
20
+ assert task.execution_times is None
20
21
  assert task.created_at > 0
21
22
 
22
23
 
@@ -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. **Review**: Read the changed files and look for quality drift that automated
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
- 3. **Consult**: Fetch the style guide with
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
- 4. **Fix**: Apply targeted, behavior-preserving fixes for the issues you find.
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
- 5. **Verify**: After any manual fix, run the relevant tests and re-run
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
- 6. **Report**: Record meaningful findings and applied fixes as progress using
53
+ 7. **Report**: Record meaningful findings and applied fixes as progress using
41
54
  `lemming progress {{finished_task_id}} '<finding>'`.
42
- 7. **No Orchestration**: Do NOT add new tasks to the roadmap. If you identify
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
- 8. **Fast Exit**: If the automated checks pass and your review finds no drift,
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(