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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/PKG-INFO +65 -11
  2. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/README.md +64 -10
  3. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/pyproject.toml +1 -1
  4. lemming_cli-0.3.4/src/lemming/brief_test.py +106 -0
  5. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/__init__.py +1 -0
  6. lemming_cli-0.3.4/src/lemming/cli/brief.py +70 -0
  7. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/config.py +11 -1
  8. lemming_cli-0.3.4/src/lemming/cli/config_test.py +114 -0
  9. lemming_cli-0.3.4/src/lemming/cli/machine_output_test.py +103 -0
  10. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/operations.py +98 -1
  11. lemming_cli-0.3.4/src/lemming/cli/stop_test.py +112 -0
  12. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/tasks.py +99 -11
  13. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/tasks_test.py +70 -1
  14. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/models.py +5 -0
  15. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/orchestrator.py +29 -1
  16. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/orchestrator_test.py +66 -0
  17. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/paths.py +19 -0
  18. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/persistence.py +44 -10
  19. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/persistence_test.py +23 -0
  20. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/taskrunner.md +1 -1
  21. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts.py +10 -0
  22. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/runner.py +115 -18
  23. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/runner_test.py +199 -3
  24. lemming_cli-0.3.4/src/lemming/shutdown.py +53 -0
  25. lemming_cli-0.3.4/src/lemming/shutdown_integration_test.py +187 -0
  26. lemming_cli-0.3.4/src/lemming/shutdown_test.py +53 -0
  27. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/__init__.py +3 -0
  28. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/lifecycle.py +26 -0
  29. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/lifecycle_test.py +29 -3
  30. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/limits.py +6 -5
  31. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/operations.py +7 -0
  32. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/operations_test.py +2 -1
  33. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/uv.lock +1 -1
  34. lemming_cli-0.3.2/src/lemming/cli/config_test.py +0 -53
  35. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.github/workflows/ci.yml +0 -0
  36. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.github/workflows/publish.yml +0 -0
  37. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.github/workflows/screenshots.yml +0 -0
  38. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.gitignore +0 -0
  39. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.prettierignore +0 -0
  40. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.prettierrc +0 -0
  41. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/Dockerfile +0 -0
  42. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/LICENSE +0 -0
  43. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/biome.json +0 -0
  44. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docker-compose.yml +0 -0
  45. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/EVALS.md +0 -0
  46. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/HOOKS.md +0 -0
  47. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/dashboard-desktop.png +0 -0
  48. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/dashboard-mobile.png +0 -0
  49. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/task-log-desktop.png +0 -0
  50. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/task-log-mobile.png +0 -0
  51. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/package-lock.json +0 -0
  52. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/package.json +0 -0
  53. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/playwright.config.js +0 -0
  54. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/__init__.py +0 -0
  55. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/__init__.py +0 -0
  56. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/auth.py +0 -0
  57. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/auth_test.py +0 -0
  58. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/config.py +0 -0
  59. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/config_test.py +0 -0
  60. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/conftest.py +0 -0
  61. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/context.py +0 -0
  62. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/context_test.py +0 -0
  63. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/directories.py +0 -0
  64. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/directories_test.py +0 -0
  65. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/files.py +0 -0
  66. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/files_test.py +0 -0
  67. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/hooks.py +0 -0
  68. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/hooks_test.py +0 -0
  69. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/logging.py +0 -0
  70. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/logging_test.py +0 -0
  71. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/loop.py +0 -0
  72. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/loop_test.py +0 -0
  73. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/main.py +0 -0
  74. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/main_test.py +0 -0
  75. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/tasks.py +0 -0
  76. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/tasks_test.py +0 -0
  77. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api_test.py +0 -0
  78. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/goal.py +0 -0
  79. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/goal_test.py +0 -0
  80. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/hooks.py +0 -0
  81. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/hooks_test.py +0 -0
  82. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/main.py +0 -0
  83. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/main_test.py +0 -0
  84. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/operations_test.py +0 -0
  85. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/progress.py +0 -0
  86. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/progress_test.py +0 -0
  87. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/readability_cli.py +0 -0
  88. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/readability_cli_test.py +0 -0
  89. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli_test.py +0 -0
  90. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/conftest.py +0 -0
  91. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/__init__.py +0 -0
  92. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/__main__.py +0 -0
  93. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/cli.py +0 -0
  94. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/cli_test.py +0 -0
  95. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/container.py +0 -0
  96. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/container_test.py +0 -0
  97. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/fixtures.py +0 -0
  98. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/fixtures_test.py +0 -0
  99. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/harness.py +0 -0
  100. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/harness_test.py +0 -0
  101. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/readability.py +0 -0
  102. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/readability_test.py +0 -0
  103. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/roadmap.py +0 -0
  104. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/roadmap_test.py +0 -0
  105. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/scenarios.py +0 -0
  106. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/scenarios_test.py +0 -0
  107. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/suites.py +0 -0
  108. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/trial.py +0 -0
  109. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/trial_test.py +0 -0
  110. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/hooks.py +0 -0
  111. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/hooks_test.py +0 -0
  112. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/integration_test.py +0 -0
  113. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/main.py +0 -0
  114. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/main_test.py +0 -0
  115. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/models_test.py +0 -0
  116. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/paths_test.py +0 -0
  117. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/50-readability.md +0 -0
  118. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/60-testing.md +0 -0
  119. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/70-ux.md +0 -0
  120. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
  121. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts_test.py +0 -0
  122. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/providers.py +0 -0
  123. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/providers_test.py +0 -0
  124. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/progress.py +0 -0
  125. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/progress_test.py +0 -0
  126. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/queries.py +0 -0
  127. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/queries_test.py +0 -0
  128. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/dashboard.spec.js +0 -0
  129. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/dashboard.test.js +0 -0
  130. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/favicon.js +0 -0
  131. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/favicon.spec.js +0 -0
  132. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/files.html +0 -0
  133. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/files.spec.js +0 -0
  134. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/index.html +0 -0
  135. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/index.js +0 -0
  136. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/logs.html +0 -0
  137. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/logs.test.js +0 -0
  138. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/mancha.js +0 -0
  139. {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/screenshots.spec.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lemming-cli
3
- Version: 0.3.2
3
+ Version: 0.3.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
@@ -247,13 +247,22 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
247
247
  ### Roadmap Management
248
248
 
249
249
  - **`status [<id>]`**: Queue/history overview or deep-dive into a specific
250
- task, including supersession lineage and runner/orchestrator-hook execution
251
- times. Superseded and failed history stays visible in the default overview;
252
- `--verbose` also shows routine completed/cancelled history.
250
+ task, including supersession lineage, the last resolved runner command, and
251
+ runner/orchestrator-hook execution times. Superseded and failed history
252
+ stays visible in the default overview; `--verbose` also shows routine
253
+ completed/cancelled history.
254
+ - `--json`: Emit machine-readable JSON instead of formatted text, so
255
+ scripts never have to parse the internal state file.
256
+ - `--brief`: Omit task descriptions, which otherwise dominate the output.
253
257
  - **`goal [<text>]`**: Set or view the long-term goal shared by all tasks.
254
258
  Supports `-f/--file`.
255
- - **`add <desc>`**: Append a new task. Supports `--index` and `--runner`.
256
- - **`edit <id>`**: Modify a task's description, runner, or position.
259
+ - **`add <desc>`**: Append a new task. Supports `--index`, `--runner`, and
260
+ `--model`.
261
+ - **`edit <id>`**: Modify a task's description, runner, model, or position.
262
+ - **`brief <id> [text]`**: View or set a task's long-form brief. Unlike the
263
+ description it has no length cap, and it is appended to the runner prompt
264
+ automatically — the right home for measured timings, exact failing
265
+ selectors, or why a previous attempt was wrong. Supports `-f/--file`.
257
266
  - **`delete <id>`**: Remove an unstarted task while retaining its runner log.
258
267
  Tasks with execution history require `--force`; autonomous restructuring
259
268
  should use `supersede`. Supports `--all` and `--completed` for bulk cleanup,
@@ -265,9 +274,11 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
265
274
  - `add <id> <finding>`: Record a new technical detail.
266
275
  - `edit <id> <index> <text>`: Modify an existing progress entry.
267
276
  - `delete <id> <index>`: Remove a progress entry.
268
- - **`config`**: Manage project configuration (runner, retries).
277
+ - **`config`**: Manage project configuration (runner, model, retries,
278
+ time limit).
269
279
  - `list`: View current configuration.
270
- - `set <key> <value>`: Update a setting.
280
+ - `set <key> <value>`: Update a setting. `set model default` clears a
281
+ pinned model without touching the runner.
271
282
  - **`hooks`**: Manage orchestrator hooks.
272
283
  - `list`: View available and active hooks.
273
284
  - `install`: Install built-in hooks to the global directory.
@@ -295,6 +306,7 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
295
306
  - **`logs [<id>]`**: Print a task's execution log to stdout, including retained
296
307
  logs for removed tasks. If no ID is provided, it defaults to the active or
297
308
  most recent task. Orchestrator hook output is automatically appended.
309
+ Supports `--json` to wrap the log with its task ID and path.
298
310
 
299
311
  ### Execution
300
312
 
@@ -303,7 +315,12 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
303
315
  - `--yolo`: Run the runner in auto-approve mode (default: True).
304
316
  - `--env`: Set environment variables for the runner (e.g., `--env KEY=VALUE`).
305
317
  - `--no-defaults`: Skip default flag injection for known runners.
306
- - `--`: Use `--` to pass any flag directly to the underlying runner.
318
+ - `--`: Use `--` to pass any flag directly to the underlying runner. A
319
+ per-task `--runner`/`--model` overrides anything passed here.
320
+ - **`stop`**: Stop the running loop and its runner.
321
+ - `--after-current-task`: Drain instead — let the running task finish, then
322
+ stop before claiming another. This is the safe way to change the runner
323
+ or model without stranding work in flight.
307
324
  - **`serve`**: Launch the interactive Web UI.
308
325
  - `--port`: The port to bind the server to (default: 8999).
309
326
  - `--host`: The host address to bind the server to (default: 127.0.0.1).
@@ -319,23 +336,60 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
319
336
  Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
320
337
  (auto-approve) and "Quiet" flags for popular tools:
321
338
 
322
- - **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions`
339
+ - **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions` and exposes
340
+ the project workspace with `--add-dir`
323
341
  - **Aider**: Adds `--yes --quiet`
324
342
  - **Claude**: Adds `--dangerously-skip-permissions`
325
343
  - **Codex**: Runs non-interactively via `codex exec`, adds `--json`, and, in
326
344
  YOLO mode, adds `--dangerously-bypass-approvals-and-sandbox`
327
345
 
346
+ ### Choosing a model
347
+
348
+ The model is a first-class setting, separate from the runner, so switching
349
+ provider does not silently discard it:
350
+
351
+ ```bash
352
+ lemming config set model gemini-3.6-flash-high # project default
353
+ lemming add "Fix the flaky test" --model fast # just this task
354
+ lemming config set model default # let the runner decide
355
+ ```
356
+
357
+ Precedence, highest first: an explicit `--model` inside a runner string, the
358
+ task's `--model`, the project's `config model`, then anything passed after
359
+ `lemming run --`. A per-task setting always wins over the loop-wide
360
+ passthrough — the conflicting global flag is dropped rather than duplicated
361
+ on the command line.
362
+
363
+ ### Runner strings
364
+
365
+ A runner is not limited to a binary name: any extra arguments in the string
366
+ are appended to the command, which is another way to pin per-task behaviour.
367
+
368
+ ```bash
369
+ lemming add "Try the fast model" --runner "agy --model fast"
370
+ ```
371
+
328
372
  You can disable default flag injection with `--no-defaults` (`codex exec`
329
373
  remains the Codex execution interface), or use a **template** to fully control
330
374
  the command layout:
331
375
 
332
376
  ```bash
333
- lemming run --runner "my-tool --input={{prompt}} --json"
377
+ lemming config set runner "my-tool --input={{prompt}} --json"
334
378
  ```
335
379
 
336
380
  When `{{prompt}}` is present in the runner string, Lemming replaces it with the
337
381
  prompt text and skips all default flag injection.
338
382
 
383
+ ### Knowing what actually ran
384
+
385
+ Each attempt records the command it launched, with the prompt elided, so the
386
+ runner and model behind a finished task stay recoverable:
387
+
388
+ ```bash
389
+ lemming status <id> # includes "Last Command:"
390
+ lemming status --json # same data, machine-readable
391
+ ```
392
+
339
393
  ---
340
394
 
341
395
  ## Releasing
@@ -225,13 +225,22 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
225
225
  ### Roadmap Management
226
226
 
227
227
  - **`status [<id>]`**: Queue/history overview or deep-dive into a specific
228
- task, including supersession lineage and runner/orchestrator-hook execution
229
- times. Superseded and failed history stays visible in the default overview;
230
- `--verbose` also shows routine completed/cancelled history.
228
+ task, including supersession lineage, the last resolved runner command, and
229
+ runner/orchestrator-hook execution times. Superseded and failed history
230
+ stays visible in the default overview; `--verbose` also shows routine
231
+ completed/cancelled history.
232
+ - `--json`: Emit machine-readable JSON instead of formatted text, so
233
+ scripts never have to parse the internal state file.
234
+ - `--brief`: Omit task descriptions, which otherwise dominate the output.
231
235
  - **`goal [<text>]`**: Set or view the long-term goal shared by all tasks.
232
236
  Supports `-f/--file`.
233
- - **`add <desc>`**: Append a new task. Supports `--index` and `--runner`.
234
- - **`edit <id>`**: Modify a task's description, runner, or position.
237
+ - **`add <desc>`**: Append a new task. Supports `--index`, `--runner`, and
238
+ `--model`.
239
+ - **`edit <id>`**: Modify a task's description, runner, model, or position.
240
+ - **`brief <id> [text]`**: View or set a task's long-form brief. Unlike the
241
+ description it has no length cap, and it is appended to the runner prompt
242
+ automatically — the right home for measured timings, exact failing
243
+ selectors, or why a previous attempt was wrong. Supports `-f/--file`.
235
244
  - **`delete <id>`**: Remove an unstarted task while retaining its runner log.
236
245
  Tasks with execution history require `--force`; autonomous restructuring
237
246
  should use `supersede`. Supports `--all` and `--completed` for bulk cleanup,
@@ -243,9 +252,11 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
243
252
  - `add <id> <finding>`: Record a new technical detail.
244
253
  - `edit <id> <index> <text>`: Modify an existing progress entry.
245
254
  - `delete <id> <index>`: Remove a progress entry.
246
- - **`config`**: Manage project configuration (runner, retries).
255
+ - **`config`**: Manage project configuration (runner, model, retries,
256
+ time limit).
247
257
  - `list`: View current configuration.
248
- - `set <key> <value>`: Update a setting.
258
+ - `set <key> <value>`: Update a setting. `set model default` clears a
259
+ pinned model without touching the runner.
249
260
  - **`hooks`**: Manage orchestrator hooks.
250
261
  - `list`: View available and active hooks.
251
262
  - `install`: Install built-in hooks to the global directory.
@@ -273,6 +284,7 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
273
284
  - **`logs [<id>]`**: Print a task's execution log to stdout, including retained
274
285
  logs for removed tasks. If no ID is provided, it defaults to the active or
275
286
  most recent task. Orchestrator hook output is automatically appended.
287
+ Supports `--json` to wrap the log with its task ID and path.
276
288
 
277
289
  ### Execution
278
290
 
@@ -281,7 +293,12 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
281
293
  - `--yolo`: Run the runner in auto-approve mode (default: True).
282
294
  - `--env`: Set environment variables for the runner (e.g., `--env KEY=VALUE`).
283
295
  - `--no-defaults`: Skip default flag injection for known runners.
284
- - `--`: Use `--` to pass any flag directly to the underlying runner.
296
+ - `--`: Use `--` to pass any flag directly to the underlying runner. A
297
+ per-task `--runner`/`--model` overrides anything passed here.
298
+ - **`stop`**: Stop the running loop and its runner.
299
+ - `--after-current-task`: Drain instead — let the running task finish, then
300
+ stop before claiming another. This is the safe way to change the runner
301
+ or model without stranding work in flight.
285
302
  - **`serve`**: Launch the interactive Web UI.
286
303
  - `--port`: The port to bind the server to (default: 8999).
287
304
  - `--host`: The host address to bind the server to (default: 127.0.0.1).
@@ -297,23 +314,60 @@ See [docs/EVALS.md](docs/EVALS.md) for details.
297
314
  Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
298
315
  (auto-approve) and "Quiet" flags for popular tools:
299
316
 
300
- - **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions`
317
+ - **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions` and exposes
318
+ the project workspace with `--add-dir`
301
319
  - **Aider**: Adds `--yes --quiet`
302
320
  - **Claude**: Adds `--dangerously-skip-permissions`
303
321
  - **Codex**: Runs non-interactively via `codex exec`, adds `--json`, and, in
304
322
  YOLO mode, adds `--dangerously-bypass-approvals-and-sandbox`
305
323
 
324
+ ### Choosing a model
325
+
326
+ The model is a first-class setting, separate from the runner, so switching
327
+ provider does not silently discard it:
328
+
329
+ ```bash
330
+ lemming config set model gemini-3.6-flash-high # project default
331
+ lemming add "Fix the flaky test" --model fast # just this task
332
+ lemming config set model default # let the runner decide
333
+ ```
334
+
335
+ Precedence, highest first: an explicit `--model` inside a runner string, the
336
+ task's `--model`, the project's `config model`, then anything passed after
337
+ `lemming run --`. A per-task setting always wins over the loop-wide
338
+ passthrough — the conflicting global flag is dropped rather than duplicated
339
+ on the command line.
340
+
341
+ ### Runner strings
342
+
343
+ A runner is not limited to a binary name: any extra arguments in the string
344
+ are appended to the command, which is another way to pin per-task behaviour.
345
+
346
+ ```bash
347
+ lemming add "Try the fast model" --runner "agy --model fast"
348
+ ```
349
+
306
350
  You can disable default flag injection with `--no-defaults` (`codex exec`
307
351
  remains the Codex execution interface), or use a **template** to fully control
308
352
  the command layout:
309
353
 
310
354
  ```bash
311
- lemming run --runner "my-tool --input={{prompt}} --json"
355
+ lemming config set runner "my-tool --input={{prompt}} --json"
312
356
  ```
313
357
 
314
358
  When `{{prompt}}` is present in the runner string, Lemming replaces it with the
315
359
  prompt text and skips all default flag injection.
316
360
 
361
+ ### Knowing what actually ran
362
+
363
+ Each attempt records the command it launched, with the prompt elided, so the
364
+ runner and model behind a finished task stay recoverable:
365
+
366
+ ```bash
367
+ lemming status <id> # includes "Last Command:"
368
+ lemming status --json # same data, machine-readable
369
+ ```
370
+
317
371
  ---
318
372
 
319
373
  ## Releasing
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1,106 @@
1
+ """Tests for the long-form task brief delivered alongside the description."""
2
+
3
+ import pathlib
4
+ import shutil
5
+ import tempfile
6
+ import unittest
7
+
8
+ import click.testing
9
+
10
+ from lemming import cli, models, paths, prompts, tasks
11
+
12
+ EVIDENCE = "Measured: first paint 2.4s. The failing selector is [data-x=1]."
13
+
14
+
15
+ class TestTaskBrief(unittest.TestCase):
16
+ def setUp(self):
17
+ self.cli_runner = click.testing.CliRunner()
18
+ self.test_dir = tempfile.mkdtemp()
19
+ self.tasks_file = pathlib.Path(self.test_dir) / "tasks.yml"
20
+ self.base_args = ["--tasks-file", str(self.tasks_file)]
21
+ self.data = models.Roadmap(
22
+ goal="Ship it",
23
+ tasks=[models.Task(id="task1", description="Fix the thing")],
24
+ )
25
+ tasks.save_tasks(self.tasks_file, self.data)
26
+
27
+ def tearDown(self):
28
+ shutil.rmtree(self.test_dir)
29
+
30
+ def test_brief_file_path_is_per_task(self):
31
+ path = paths.get_brief_file(self.tasks_file, "task1")
32
+
33
+ self.assertTrue(str(path).endswith("task1-brief.md"))
34
+
35
+ def test_prompt_includes_brief_when_present(self):
36
+ """The runner must receive the evidence without being told to look."""
37
+ paths.get_brief_file(self.tasks_file, "task1").write_text(EVIDENCE)
38
+
39
+ prompt = prompts.prepare_prompt(
40
+ self.data, self.data.tasks[0], self.tasks_file
41
+ )
42
+
43
+ self.assertIn(EVIDENCE, prompt)
44
+
45
+ def test_prompt_omits_brief_section_when_absent(self):
46
+ """No brief means no empty scaffolding in the prompt."""
47
+ prompt = prompts.prepare_prompt(
48
+ self.data, self.data.tasks[0], self.tasks_file
49
+ )
50
+
51
+ self.assertNotIn("Task Brief", prompt)
52
+
53
+ def test_brief_command_writes_and_reads_back(self):
54
+ write = self.cli_runner.invoke(
55
+ cli.cli, self.base_args + ["brief", "task1", EVIDENCE]
56
+ )
57
+ self.assertEqual(write.exit_code, 0, write.output)
58
+
59
+ read = self.cli_runner.invoke(
60
+ cli.cli, self.base_args + ["brief", "task1"]
61
+ )
62
+
63
+ self.assertEqual(read.exit_code, 0, read.output)
64
+ self.assertIn(EVIDENCE, read.output)
65
+
66
+ def test_brief_command_reads_from_stdin(self):
67
+ result = self.cli_runner.invoke(
68
+ cli.cli,
69
+ self.base_args + ["brief", "task1", "--file", "-"],
70
+ input=EVIDENCE,
71
+ )
72
+
73
+ self.assertEqual(result.exit_code, 0, result.output)
74
+ stored = paths.get_brief_file(self.tasks_file, "task1").read_text()
75
+ self.assertIn(EVIDENCE, stored)
76
+
77
+ def test_brief_reports_when_missing(self):
78
+ result = self.cli_runner.invoke(
79
+ cli.cli, self.base_args + ["brief", "task1"]
80
+ )
81
+
82
+ self.assertEqual(result.exit_code, 0, result.output)
83
+ self.assertIn("No brief", result.output)
84
+
85
+ def test_brief_has_no_length_cap(self):
86
+ """The brief is where evidence too large for a description belongs."""
87
+ long_evidence = "y" * (tasks.MAX_TASK_DESCRIPTION_CHARS * 3)
88
+
89
+ result = self.cli_runner.invoke(
90
+ cli.cli, self.base_args + ["brief", "task1", long_evidence]
91
+ )
92
+
93
+ self.assertEqual(result.exit_code, 0, result.output)
94
+ stored = paths.get_brief_file(self.tasks_file, "task1").read_text()
95
+ self.assertEqual(len(stored.strip()), len(long_evidence))
96
+
97
+ def test_description_cap_error_points_at_the_brief(self):
98
+ """Hitting the cap must name the supported way to attach evidence."""
99
+ with self.assertRaises(ValueError) as caught:
100
+ tasks.add_task(self.tasks_file, "z" * 5000)
101
+
102
+ self.assertIn("lemming brief", str(caught.exception))
103
+
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main()
@@ -1,5 +1,6 @@
1
1
  """Lemming CLI package; importing the submodules registers all commands."""
2
2
 
3
+ from . import brief as _brief_cmds # noqa: F401
3
4
  from . import config as _config_cmds # noqa: F401
4
5
  from . import goal as _goal_cmds # noqa: F401
5
6
  from . import hooks as _hooks_cmds # noqa: F401
@@ -0,0 +1,70 @@
1
+ """CLI command for the long-form task brief."""
2
+
3
+ import typing
4
+
5
+ import click
6
+
7
+ from .. import paths, tasks
8
+ from .main import cli
9
+
10
+
11
+ @cli.command(short_help="<taskid> [text] View or set a task's long-form brief")
12
+ @click.argument("task_id")
13
+ @click.argument("text", required=False)
14
+ @click.option(
15
+ "--file",
16
+ "-f",
17
+ type=click.File("r"),
18
+ help="Read the brief from a file (or - for stdin).",
19
+ )
20
+ @click.pass_context
21
+ def brief(
22
+ ctx: click.Context,
23
+ task_id: str,
24
+ text: typing.Optional[str],
25
+ file: typing.Optional[typing.TextIO],
26
+ ):
27
+ """Views or sets the long-form brief delivered with a task.
28
+
29
+ The brief holds evidence that does not belong in the description: measured
30
+ timings, exact failing selectors, why a previous attempt was wrong. It has
31
+ no length cap and is appended to the runner prompt automatically, so the
32
+ description never has to point at it.
33
+
34
+ Examples:
35
+ lemming brief a1b2c3d4
36
+ lemming brief a1b2c3d4 "Timings: first paint 2.4s"
37
+ lemming brief a1b2c3d4 --file notes.md
38
+ """
39
+ tasks_file = ctx.obj["TASKS_FILE"]
40
+
41
+ data = tasks.load_tasks(tasks_file)
42
+ try:
43
+ target = tasks.resolve_task(data.tasks, task_id)
44
+ except (tasks.TaskNotFoundError, tasks.AmbiguousTaskIdError) as e:
45
+ click.echo(f"Error: {e}")
46
+ ctx.exit(1)
47
+
48
+ if file and text:
49
+ click.echo("Error: Cannot provide both brief text and --file.")
50
+ ctx.exit(1)
51
+
52
+ brief_file = paths.get_brief_file(tasks_file, target.id)
53
+
54
+ # With no new content, the command reads the brief back.
55
+ if not file and not text:
56
+ if not brief_file.exists() or not brief_file.read_text().strip():
57
+ click.echo(f"No brief for task {target.id}.")
58
+ return
59
+ click.echo(brief_file.read_text(encoding="utf-8"))
60
+ return
61
+
62
+ content = (file.read() if file else text or "").strip()
63
+ if not content:
64
+ click.echo("Error: Must provide either brief text or --file.")
65
+ ctx.exit(1)
66
+
67
+ brief_file.write_text(content + "\n", encoding="utf-8")
68
+ click.echo(
69
+ f"Brief for task {target.id} saved ({len(content):,} characters)."
70
+ )
@@ -23,6 +23,7 @@ def config_list(ctx: click.Context):
23
23
 
24
24
  click.secho(f"Configuration for {tasks_file}:", bold=True)
25
25
  click.echo(f" Runner: {c.runner}")
26
+ click.echo(f" Model: {c.model or '(runner default)'}")
26
27
  click.echo(f" Retries: {c.retries}")
27
28
  click.echo(f" Time limit: {format_duration(c.time_limit)}")
28
29
 
@@ -37,15 +38,21 @@ def config_list(ctx: click.Context):
37
38
  @config_group.command(name="set")
38
39
  @click.argument(
39
40
  "key",
40
- type=click.Choice(["runner", "retries", "time_limit"]),
41
+ type=click.Choice(["runner", "model", "retries", "time_limit"]),
41
42
  )
42
43
  @click.argument("value")
43
44
  @click.pass_context
44
45
  def config_set(ctx: click.Context, key: str, value: str):
45
46
  """Sets a configuration value.
46
47
 
48
+ The runner is the CLI to invoke; the model is a separate field so that
49
+ switching runners (for example from a quota hook) does not silently
50
+ discard the pinned model.
51
+
47
52
  Examples:
48
53
  lemming config set runner aider
54
+ lemming config set model gemini-3.6-flash-high
55
+ lemming config set model default
49
56
  lemming config set retries 5
50
57
  lemming config set time_limit 30m
51
58
  """
@@ -54,6 +61,9 @@ def config_set(ctx: click.Context, key: str, value: str):
54
61
 
55
62
  if key == "runner":
56
63
  data.config.runner = value
64
+ elif key == "model":
65
+ # "default" clears the pin and lets the runner choose.
66
+ data.config.model = None if value.lower() == "default" else value
57
67
  elif key == "retries":
58
68
  try:
59
69
  data.config.retries = int(value)
@@ -0,0 +1,114 @@
1
+ import pathlib
2
+ import shutil
3
+ import tempfile
4
+ import unittest
5
+
6
+ import click.testing
7
+
8
+ from lemming import cli, tasks
9
+
10
+
11
+ class TestCLIConfig(unittest.TestCase):
12
+ def setUp(self):
13
+ self.cli_runner = click.testing.CliRunner()
14
+ self.test_dir = tempfile.mkdtemp()
15
+ self.test_tasks_file = pathlib.Path(self.test_dir) / "tasks_test.yml"
16
+ self.base_args = [
17
+ "--verbose",
18
+ "--tasks-file",
19
+ str(self.test_tasks_file),
20
+ ]
21
+
22
+ # Scaffold a valid file
23
+ data = tasks.Roadmap(
24
+ goal="Initial goal",
25
+ tasks=[],
26
+ )
27
+ tasks.save_tasks(self.test_tasks_file, data)
28
+
29
+ def tearDown(self):
30
+ shutil.rmtree(self.test_dir)
31
+
32
+ def test_config_list(self):
33
+ result = self.cli_runner.invoke(
34
+ cli.cli, self.base_args + ["config", "list"]
35
+ )
36
+ self.assertEqual(result.exit_code, 0)
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)
41
+
42
+ def test_config_set(self):
43
+ result = self.cli_runner.invoke(
44
+ cli.cli, self.base_args + ["config", "set", "runner", "new-runner"]
45
+ )
46
+ self.assertEqual(result.exit_code, 0)
47
+ self.assertIn("Updated runner to new-runner", result.output)
48
+ data = tasks.load_tasks(self.test_tasks_file)
49
+ self.assertEqual(data.config.runner, "new-runner")
50
+
51
+
52
+ if __name__ == "__main__":
53
+ unittest.main()
54
+
55
+
56
+ class TestCLIConfigModel(unittest.TestCase):
57
+ def setUp(self):
58
+ self.cli_runner = click.testing.CliRunner()
59
+ self.test_dir = tempfile.mkdtemp()
60
+ self.test_tasks_file = pathlib.Path(self.test_dir) / "tasks_test.yml"
61
+ self.base_args = ["--tasks-file", str(self.test_tasks_file)]
62
+ tasks.save_tasks(self.test_tasks_file, tasks.Roadmap(goal="g"))
63
+
64
+ def tearDown(self):
65
+ shutil.rmtree(self.test_dir)
66
+
67
+ def test_config_set_model_persists(self):
68
+ """The model is a first-class field, not part of the runner string."""
69
+ result = self.cli_runner.invoke(
70
+ cli.cli, self.base_args + ["config", "set", "model", "fast-model"]
71
+ )
72
+
73
+ self.assertEqual(result.exit_code, 0, result.output)
74
+ data = tasks.load_tasks(self.test_tasks_file)
75
+ self.assertEqual(data.config.model, "fast-model")
76
+
77
+ def test_switching_runner_preserves_model(self):
78
+ """A quota hook switching runners must not discard the model."""
79
+ self.cli_runner.invoke(
80
+ cli.cli, self.base_args + ["config", "set", "model", "fast-model"]
81
+ )
82
+
83
+ self.cli_runner.invoke(
84
+ cli.cli, self.base_args + ["config", "set", "runner", "codex"]
85
+ )
86
+
87
+ data = tasks.load_tasks(self.test_tasks_file)
88
+ self.assertEqual(data.config.runner, "codex")
89
+ self.assertEqual(data.config.model, "fast-model")
90
+
91
+ def test_config_set_model_default_clears_pin(self):
92
+ """ "default" hands model choice back to the runner."""
93
+ self.cli_runner.invoke(
94
+ cli.cli, self.base_args + ["config", "set", "model", "fast-model"]
95
+ )
96
+
97
+ self.cli_runner.invoke(
98
+ cli.cli, self.base_args + ["config", "set", "model", "default"]
99
+ )
100
+
101
+ data = tasks.load_tasks(self.test_tasks_file)
102
+ self.assertIsNone(data.config.model)
103
+
104
+ def test_config_list_shows_model(self):
105
+ """The model is discoverable without reading the source."""
106
+ self.cli_runner.invoke(
107
+ cli.cli, self.base_args + ["config", "set", "model", "fast-model"]
108
+ )
109
+
110
+ result = self.cli_runner.invoke(
111
+ cli.cli, self.base_args + ["config", "list"]
112
+ )
113
+
114
+ self.assertIn("fast-model", result.output)