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.
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/PKG-INFO +65 -11
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/README.md +64 -10
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/pyproject.toml +1 -1
- lemming_cli-0.3.4/src/lemming/brief_test.py +106 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/__init__.py +1 -0
- lemming_cli-0.3.4/src/lemming/cli/brief.py +70 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/config.py +11 -1
- lemming_cli-0.3.4/src/lemming/cli/config_test.py +114 -0
- lemming_cli-0.3.4/src/lemming/cli/machine_output_test.py +103 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/operations.py +98 -1
- lemming_cli-0.3.4/src/lemming/cli/stop_test.py +112 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/tasks.py +99 -11
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/tasks_test.py +70 -1
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/models.py +5 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/orchestrator.py +29 -1
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/orchestrator_test.py +66 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/paths.py +19 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/persistence.py +44 -10
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/persistence_test.py +23 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/taskrunner.md +1 -1
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts.py +10 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/runner.py +115 -18
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/runner_test.py +199 -3
- lemming_cli-0.3.4/src/lemming/shutdown.py +53 -0
- lemming_cli-0.3.4/src/lemming/shutdown_integration_test.py +187 -0
- lemming_cli-0.3.4/src/lemming/shutdown_test.py +53 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/__init__.py +3 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/lifecycle.py +26 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/lifecycle_test.py +29 -3
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/limits.py +6 -5
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/operations.py +7 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/operations_test.py +2 -1
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/uv.lock +1 -1
- lemming_cli-0.3.2/src/lemming/cli/config_test.py +0 -53
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.github/workflows/ci.yml +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.github/workflows/publish.yml +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.github/workflows/screenshots.yml +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.gitignore +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.prettierignore +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/.prettierrc +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/Dockerfile +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/LICENSE +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/biome.json +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docker-compose.yml +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/EVALS.md +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/HOOKS.md +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/dashboard-desktop.png +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/dashboard-mobile.png +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/task-log-desktop.png +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/docs/screenshots/task-log-mobile.png +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/package-lock.json +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/package.json +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/playwright.config.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/__init__.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/__init__.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/auth.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/auth_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/config.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/config_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/conftest.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/context.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/context_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/directories.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/directories_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/files.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/files_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/hooks.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/hooks_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/logging.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/logging_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/loop.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/loop_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/main.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/main_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/tasks.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api/tasks_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/api_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/goal.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/goal_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/hooks.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/hooks_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/main.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/main_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/operations_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/progress.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/progress_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/readability_cli.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli/readability_cli_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/cli_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/conftest.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/__init__.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/__main__.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/cli.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/cli_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/container.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/container_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/fixtures.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/fixtures_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/harness.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/harness_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/readability.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/readability_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/roadmap.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/roadmap_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/scenarios.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/scenarios_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/suites.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/trial.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/evals/trial_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/hooks.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/hooks_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/integration_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/main.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/main_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/models_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/paths_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/50-readability.md +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/60-testing.md +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/70-ux.md +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/prompts_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/providers.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/providers_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/progress.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/progress_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/queries.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/tasks/queries_test.py +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/dashboard.spec.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/dashboard.test.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/favicon.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/favicon.spec.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/files.html +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/files.spec.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/index.html +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/index.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/logs.html +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/logs.test.js +0 -0
- {lemming_cli-0.3.2 → lemming_cli-0.3.4}/src/lemming/web/mancha.js +0 -0
- {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.
|
|
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
|
|
251
|
-
times. Superseded and failed history
|
|
252
|
-
`--verbose` also shows routine
|
|
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
|
|
256
|
-
|
|
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
|
|
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
|
|
229
|
-
times. Superseded and failed history
|
|
230
|
-
`--verbose` also shows routine
|
|
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
|
|
234
|
-
|
|
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
|
|
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
|
|
@@ -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)
|