lemming-cli 0.4.2__tar.gz → 0.4.3__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 (146) hide show
  1. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/Dockerfile +2 -1
  2. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/PKG-INFO +14 -11
  3. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/README.md +12 -9
  4. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/SKILL.md +2 -2
  5. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/pyproject.toml +2 -2
  6. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/config_test.py +1 -1
  7. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/config.py +1 -1
  8. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/container.py +1 -0
  9. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/models.py +1 -1
  10. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/runner.py +65 -8
  11. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/runner_test.py +103 -2
  12. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/dashboard.spec.js +1 -1
  13. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/favicon.spec.js +1 -1
  14. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/screenshots.spec.js +2 -2
  15. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/uv.lock +1 -1
  16. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/.dockerignore +0 -0
  17. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/.github/workflows/ci.yml +0 -0
  18. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/.github/workflows/publish.yml +0 -0
  19. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/.github/workflows/screenshots.yml +0 -0
  20. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/.gitignore +0 -0
  21. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/.prettierignore +0 -0
  22. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/.prettierrc +0 -0
  23. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/LICENSE +0 -0
  24. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/biome.json +0 -0
  25. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/docker-compose.yml +0 -0
  26. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/docs/EVALS.md +0 -0
  27. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/docs/HOOKS.md +0 -0
  28. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/docs/screenshots/dashboard-desktop.png +0 -0
  29. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/docs/screenshots/dashboard-mobile.png +0 -0
  30. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/docs/screenshots/task-log-desktop.png +0 -0
  31. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/docs/screenshots/task-log-mobile.png +0 -0
  32. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/package-lock.json +0 -0
  33. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/package.json +0 -0
  34. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/playwright.config.js +0 -0
  35. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/__init__.py +0 -0
  36. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/__init__.py +0 -0
  37. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/auth.py +0 -0
  38. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/auth_test.py +0 -0
  39. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/config.py +0 -0
  40. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/conftest.py +0 -0
  41. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/context.py +0 -0
  42. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/context_test.py +0 -0
  43. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/directories.py +0 -0
  44. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/directories_test.py +0 -0
  45. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/files.py +0 -0
  46. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/files_test.py +0 -0
  47. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/hooks.py +0 -0
  48. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/hooks_test.py +0 -0
  49. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/logging.py +0 -0
  50. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/logging_test.py +0 -0
  51. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/loop.py +0 -0
  52. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/loop_test.py +0 -0
  53. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/main.py +0 -0
  54. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/main_test.py +0 -0
  55. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/tasks.py +0 -0
  56. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api/tasks_test.py +0 -0
  57. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/api_test.py +0 -0
  58. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/brief_test.py +0 -0
  59. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/__init__.py +0 -0
  60. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/brief.py +0 -0
  61. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/config_test.py +0 -0
  62. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/exec_cli.py +0 -0
  63. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/exec_cli_test.py +0 -0
  64. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/goal.py +0 -0
  65. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/goal_test.py +0 -0
  66. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/hooks.py +0 -0
  67. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/hooks_test.py +0 -0
  68. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/machine_output_test.py +0 -0
  69. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/main.py +0 -0
  70. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/main_test.py +0 -0
  71. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/operations.py +0 -0
  72. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/operations_test.py +0 -0
  73. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/progress.py +0 -0
  74. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/progress_test.py +0 -0
  75. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/readability_cli.py +0 -0
  76. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/readability_cli_test.py +0 -0
  77. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/skill.py +0 -0
  78. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/skill_test.py +0 -0
  79. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/stop_test.py +0 -0
  80. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/tasks.py +0 -0
  81. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli/tasks_test.py +0 -0
  82. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/cli_test.py +0 -0
  83. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/conftest.py +0 -0
  84. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/__init__.py +0 -0
  85. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/__main__.py +0 -0
  86. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/cli.py +0 -0
  87. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/cli_test.py +0 -0
  88. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/container_test.py +0 -0
  89. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/fixtures.py +0 -0
  90. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/fixtures_test.py +0 -0
  91. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/harness.py +0 -0
  92. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/harness_test.py +0 -0
  93. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/readability.py +0 -0
  94. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/readability_test.py +0 -0
  95. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/roadmap.py +0 -0
  96. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/roadmap_test.py +0 -0
  97. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/scenarios.py +0 -0
  98. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/scenarios_test.py +0 -0
  99. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/suites.py +0 -0
  100. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/trial.py +0 -0
  101. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/evals/trial_test.py +0 -0
  102. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/hooks.py +0 -0
  103. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/hooks_test.py +0 -0
  104. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/integration_test.py +0 -0
  105. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/main.py +0 -0
  106. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/main_test.py +0 -0
  107. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/models_test.py +0 -0
  108. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/orchestrator.py +0 -0
  109. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/orchestrator_test.py +0 -0
  110. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/paths.py +0 -0
  111. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/paths_test.py +0 -0
  112. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/persistence.py +0 -0
  113. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/persistence_test.py +0 -0
  114. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/prompts/hooks/50-readability.md +0 -0
  115. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/prompts/hooks/60-testing.md +0 -0
  116. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/prompts/hooks/70-ux.md +0 -0
  117. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
  118. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/prompts/taskrunner.md +0 -0
  119. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/prompts.py +0 -0
  120. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/prompts_test.py +0 -0
  121. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/providers.py +0 -0
  122. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/providers_test.py +0 -0
  123. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/scope.py +0 -0
  124. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/scope_test.py +0 -0
  125. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/shutdown.py +0 -0
  126. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/shutdown_integration_test.py +0 -0
  127. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/shutdown_test.py +0 -0
  128. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/__init__.py +0 -0
  129. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/lifecycle.py +0 -0
  130. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/lifecycle_test.py +0 -0
  131. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/limits.py +0 -0
  132. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/operations.py +0 -0
  133. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/operations_test.py +0 -0
  134. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/progress.py +0 -0
  135. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/progress_test.py +0 -0
  136. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/queries.py +0 -0
  137. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/tasks/queries_test.py +0 -0
  138. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/dashboard.test.js +0 -0
  139. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/favicon.js +0 -0
  140. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/files.html +0 -0
  141. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/files.spec.js +0 -0
  142. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/index.html +0 -0
  143. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/index.js +0 -0
  144. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/logs.html +0 -0
  145. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/logs.test.js +0 -0
  146. {lemming_cli-0.4.2 → lemming_cli-0.4.3}/src/lemming/web/mancha.js +0 -0
@@ -11,7 +11,8 @@ RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
11
11
  && rm -rf /var/lib/apt/lists/*
12
12
 
13
13
  # Install AI coding CLIs (antigravity ships a native binary, not an npm package)
14
- RUN npm install -g @openai/codex @anthropic-ai/claude-code \
14
+ RUN npm install -g --allow-scripts=opencode-ai \
15
+ @openai/codex @anthropic-ai/claude-code opencode-ai \
15
16
  && curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir /usr/local/bin
16
17
 
17
18
  # Copy cloudflared and uv binaries directly from their official images
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lemming-cli
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
7
7
  Author: Lemming Contributors
8
8
  License: MIT
9
9
  License-File: LICENSE
10
- Keywords: agents,ai,aider,antigravity,automation,claude
10
+ Keywords: agents,ai,antigravity,automation,claude,opencode
11
11
  Requires-Python: >=3.12
12
12
  Requires-Dist: beautifulsoup4>=4.13.3
13
13
  Requires-Dist: click>=8.0.0
@@ -38,8 +38,8 @@ human-readable `tasks.yml` file.
38
38
  update is recorded in a human-readable `tasks.yml` file. You can step in,
39
39
  adjust the roadmap, or swap agents at any time.
40
40
  - **Tool Agnostic**: Lemming doesn't care which agent you use. It works
41
- out-of-the-box with `agy`, `aider`, `claude`, `codex`, or even your own custom
42
- scripts.
41
+ out-of-the-box with `agy`, `opencode`, `claude`, `codex`, or even your own
42
+ custom scripts.
43
43
  - **Resilient Execution**: With built-in heartbeat monitoring, automatic
44
44
  retries, and progress tracking, Lemming handles process crashes and rate
45
45
  limits gracefully.
@@ -93,7 +93,7 @@ Start the autonomous loop.
93
93
 
94
94
  ```bash
95
95
  # Run using the project's configured agent (defaults to the first
96
- # supported runner found on PATH: agy, aider, claude, or codex)
96
+ # supported runner found on PATH: agy, opencode, claude, or codex)
97
97
  lemming run
98
98
 
99
99
  # Flags passed after -- are sent directly to the underlying runner
@@ -150,15 +150,15 @@ inherit the permission prompts of whatever launched it.
150
150
 
151
151
  ### Teaching Your Agent to Use It
152
152
 
153
- Lemming ships an Agent Skill so that an agent already running — Claude Code,
154
- Gemini CLI, Cursor — can reach any other agent CLI without knowing how each
155
- one spells its flags. Install it once:
153
+ Lemming ships an Agent Skill so that an agent already running — Codex, Claude
154
+ Code, Gemini CLI, Cursor — can reach any other agent CLI without knowing how
155
+ each one spells its flags. Install it once:
156
156
 
157
157
  ```bash
158
158
  # Cross-tool location (~/.agents/skills), and report any tool-specific ones
159
159
  lemming skill install
160
160
 
161
- # Cover every agent tool detected on this machine
161
+ # Cover detected tools that require their own skill directory too
162
162
  lemming skill install --all
163
163
 
164
164
  # This repository only
@@ -293,7 +293,7 @@ lemming hooks list
293
293
  lemming config list
294
294
 
295
295
  # Persist configuration to tasks.yml
296
- lemming config set runner aider
296
+ lemming config set runner opencode
297
297
  ```
298
298
 
299
299
  ### Evaluating Prompt Changes
@@ -475,7 +475,10 @@ Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
475
475
 
476
476
  - **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions` and exposes
477
477
  the project workspace with `--add-dir`
478
- - **Aider**: Adds `--yes --quiet`
478
+ - **OpenCode**: Runs non-interactively via `opencode run`, adds `--format json`,
479
+ and, in YOLO mode, adds `--auto`. For its Google provider, Lemming makes an
480
+ existing `GOOGLE_API_KEY` or `GEMINI_API_KEY` available under OpenCode's
481
+ native `GOOGLE_GENERATIVE_AI_API_KEY` name.
479
482
  - **Claude**: Adds `--dangerously-skip-permissions`
480
483
  - **Codex**: Runs non-interactively via `codex exec`, adds `--json`, and, in
481
484
  YOLO mode, adds `--dangerously-bypass-approvals-and-sandbox`
@@ -16,8 +16,8 @@ human-readable `tasks.yml` file.
16
16
  update is recorded in a human-readable `tasks.yml` file. You can step in,
17
17
  adjust the roadmap, or swap agents at any time.
18
18
  - **Tool Agnostic**: Lemming doesn't care which agent you use. It works
19
- out-of-the-box with `agy`, `aider`, `claude`, `codex`, or even your own custom
20
- scripts.
19
+ out-of-the-box with `agy`, `opencode`, `claude`, `codex`, or even your own
20
+ custom scripts.
21
21
  - **Resilient Execution**: With built-in heartbeat monitoring, automatic
22
22
  retries, and progress tracking, Lemming handles process crashes and rate
23
23
  limits gracefully.
@@ -71,7 +71,7 @@ Start the autonomous loop.
71
71
 
72
72
  ```bash
73
73
  # Run using the project's configured agent (defaults to the first
74
- # supported runner found on PATH: agy, aider, claude, or codex)
74
+ # supported runner found on PATH: agy, opencode, claude, or codex)
75
75
  lemming run
76
76
 
77
77
  # Flags passed after -- are sent directly to the underlying runner
@@ -128,15 +128,15 @@ inherit the permission prompts of whatever launched it.
128
128
 
129
129
  ### Teaching Your Agent to Use It
130
130
 
131
- Lemming ships an Agent Skill so that an agent already running — Claude Code,
132
- Gemini CLI, Cursor — can reach any other agent CLI without knowing how each
133
- one spells its flags. Install it once:
131
+ Lemming ships an Agent Skill so that an agent already running — Codex, Claude
132
+ Code, Gemini CLI, Cursor — can reach any other agent CLI without knowing how
133
+ each one spells its flags. Install it once:
134
134
 
135
135
  ```bash
136
136
  # Cross-tool location (~/.agents/skills), and report any tool-specific ones
137
137
  lemming skill install
138
138
 
139
- # Cover every agent tool detected on this machine
139
+ # Cover detected tools that require their own skill directory too
140
140
  lemming skill install --all
141
141
 
142
142
  # This repository only
@@ -271,7 +271,7 @@ lemming hooks list
271
271
  lemming config list
272
272
 
273
273
  # Persist configuration to tasks.yml
274
- lemming config set runner aider
274
+ lemming config set runner opencode
275
275
  ```
276
276
 
277
277
  ### Evaluating Prompt Changes
@@ -453,7 +453,10 @@ Lemming uses **fuzzy matching** to automatically inject the correct "YOLO"
453
453
 
454
454
  - **Antigravity (`agy`)**: Adds `--dangerously-skip-permissions` and exposes
455
455
  the project workspace with `--add-dir`
456
- - **Aider**: Adds `--yes --quiet`
456
+ - **OpenCode**: Runs non-interactively via `opencode run`, adds `--format json`,
457
+ and, in YOLO mode, adds `--auto`. For its Google provider, Lemming makes an
458
+ existing `GOOGLE_API_KEY` or `GEMINI_API_KEY` available under OpenCode's
459
+ native `GOOGLE_GENERATIVE_AI_API_KEY` name.
457
460
  - **Claude**: Adds `--dangerously-skip-permissions`
458
461
  - **Codex**: Runs non-interactively via `codex exec`, adds `--json`, and, in
459
462
  YOLO mode, adds `--dangerously-bypass-approvals-and-sandbox`
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: lemming
3
- description: One interface over the AI coding agent CLIs (agy, claude, codex). Delegate a single task to another agent to spare a quota or play to its strengths; run a readability, testing, or UX review over changed files, a branch, or a path; or drive a multi-task project through a roadmap that survives context limits. Use whenever work should run under a different agent than the current one, when reviewing a diff before opening a pull request, or when a job is too large for one agent run.
3
+ description: One interface over the AI coding agent CLIs (agy, claude, codex, opencode). Delegate a single task to another agent to spare a quota or play to its strengths; run a readability, testing, or UX review over changed files, a branch, or a path; or drive a multi-task project through a roadmap that survives context limits. Use whenever work should run under a different agent than the current one, when reviewing a diff before opening a pull request, or when a job is too large for one agent run.
4
4
  license: MIT
5
5
  ---
6
6
 
7
7
  # lemming
8
8
 
9
9
  One interface over several agent CLIs. You do not need to know how `agy`,
10
- `claude`, or `codex` spell their flags — only `lemming`.
10
+ `claude`, `codex`, or `opencode` spell their flags — only `lemming`.
11
11
 
12
12
  Use `exec` for one unit of work, and a roadmap for anything larger than a
13
13
  single agent run can hold.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lemming-cli"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "An autonomous, iterative task runner for AI coding agents."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -12,7 +12,7 @@ authors = [
12
12
  ]
13
13
  license = { text = "MIT" }
14
14
  requires-python = ">=3.12"
15
- keywords = ["ai", "agents", "automation", "antigravity", "claude", "aider"]
15
+ keywords = ["ai", "agents", "automation", "antigravity", "claude", "opencode"]
16
16
  dependencies = [
17
17
  "beautifulsoup4>=4.13.3",
18
18
  "click>=8.0.0",
@@ -20,7 +20,7 @@ def test_run_loop(client, test_tasks):
20
20
  def test_get_runners(client):
21
21
  response = client.get("/api/runners")
22
22
  assert response.status_code == 200
23
- assert response.json() == ["agy", "aider", "claude", "codex"]
23
+ assert response.json() == ["agy", "opencode", "claude", "codex"]
24
24
 
25
25
 
26
26
  def test_project_goal_isolation(client, test_tasks):
@@ -50,7 +50,7 @@ def config_set(ctx: click.Context, key: str, value: str):
50
50
  discard the pinned model.
51
51
 
52
52
  Examples:
53
- lemming config set runner aider
53
+ lemming config set runner opencode
54
54
  lemming config set model gemini-3.6-flash-high
55
55
  lemming config set model default
56
56
  lemming config set retries 5
@@ -19,6 +19,7 @@ DEFAULT_FORWARD_ENV = (
19
19
  "CLAUDE_CODE_OAUTH_TOKEN",
20
20
  "GEMINI_API_KEY",
21
21
  "GOOGLE_API_KEY",
22
+ "GOOGLE_GENERATIVE_AI_API_KEY",
22
23
  )
23
24
 
24
25
  # Paths inside the container; the harness mounts host dirs onto these.
@@ -8,7 +8,7 @@ import time
8
8
  import pydantic
9
9
 
10
10
  # Runner CLIs the orchestrator knows how to drive, in default-preference order.
11
- KNOWN_RUNNERS = ("agy", "aider", "claude", "codex")
11
+ KNOWN_RUNNERS = ("agy", "opencode", "claude", "codex")
12
12
 
13
13
 
14
14
  @functools.cache
@@ -209,15 +209,21 @@ def build_runner_command(
209
209
 
210
210
  runner_base = os.path.basename(parts[0])
211
211
  is_codex = runner_base.startswith("codex")
212
+ is_opencode = runner_base.startswith("opencode")
212
213
 
213
- # Codex's non-interactive interface is the ``exec`` subcommand. Accept an
214
- # explicitly configured ``codex exec``/``codex e`` without duplicating the
215
- # subcommand, while still making a bare ``codex`` suitable for automation.
214
+ # Codex and OpenCode expose their non-interactive interfaces as
215
+ # subcommands. Accept explicitly configured forms without duplicating the
216
+ # subcommand, while still making a bare executable suitable for automation.
216
217
  if is_codex:
217
218
  if extra_parts and extra_parts[0] in {"exec", "e"}:
218
219
  cmd.append(extra_parts.pop(0))
219
220
  else:
220
221
  cmd.append("exec")
222
+ elif is_opencode:
223
+ if extra_parts and extra_parts[0] == "run":
224
+ cmd.append(extra_parts.pop(0))
225
+ else:
226
+ cmd.append("run")
221
227
 
222
228
  if not no_defaults:
223
229
  if runner_base.startswith("agy"):
@@ -252,11 +258,19 @@ def build_runner_command(
252
258
 
253
259
  prompt_arg = "--prompt"
254
260
  elif runner_base.startswith("aider"):
261
+ # Backward compatibility for persisted/custom Aider runners. Aider
262
+ # is no longer advertised as a first-class runner.
255
263
  if yolo:
256
264
  cmd.append("--yes")
257
265
  if not verbose:
258
266
  cmd.append("--quiet")
259
267
  prompt_arg = "--message"
268
+ elif is_opencode:
269
+ if yolo:
270
+ cmd.append("--auto")
271
+ # Raw JSON events expose completed text, tool activity, and errors
272
+ # in Lemming's live task log.
273
+ cmd.extend(["--format", "json"])
260
274
  elif runner_base.startswith("claude"):
261
275
  if yolo:
262
276
  cmd.append("--dangerously-skip-permissions")
@@ -342,8 +356,19 @@ def extract_error_message(
342
356
 
343
357
  kind = event.get("type")
344
358
  message = None
345
- if kind == "error" and isinstance(event.get("message"), str):
346
- message = event["message"]
359
+ if kind == "error":
360
+ if isinstance(event.get("message"), str):
361
+ message = event["message"]
362
+ else:
363
+ # OpenCode's JSON stream carries the provider error object
364
+ # under ``error``, usually with its text in ``data.message``.
365
+ nested = event.get("error")
366
+ if isinstance(nested, dict):
367
+ data = nested.get("data")
368
+ if isinstance(data, dict):
369
+ message = data.get("message")
370
+ if not isinstance(message, str):
371
+ message = nested.get("message")
347
372
  elif kind == "turn.failed":
348
373
  nested = event.get("error")
349
374
  if isinstance(nested, dict):
@@ -402,6 +427,16 @@ def _final_message_text(event: dict) -> str | None:
402
427
  response = result.get("response")
403
428
  return response if isinstance(response, str) else None
404
429
 
430
+ # OpenCode emits completed assistant text as its own event type. Partial
431
+ # text is not emitted in JSON mode, so the last such event is the closing
432
+ # response even when tool and step events follow it.
433
+ if event.get("type") == "text":
434
+ part = event.get("part")
435
+ if not isinstance(part, dict) or part.get("type") != "text":
436
+ return None
437
+ text = part.get("text")
438
+ return text if isinstance(text, str) else None
439
+
405
440
  return None
406
441
 
407
442
 
@@ -493,6 +528,30 @@ def _kill_process_tree(process: subprocess.Popen) -> None:
493
528
  pass
494
529
 
495
530
 
531
+ def _runner_environment(
532
+ cmd: list[str], tasks_file: pathlib.Path, task_id: str
533
+ ) -> dict[str, str]:
534
+ """Builds the subprocess environment, including runner compatibility.
535
+
536
+ OpenCode's native Google provider uses the longer environment variable
537
+ name, while existing Lemming eval and user environments commonly expose
538
+ ``GOOGLE_API_KEY`` or ``GEMINI_API_KEY``. Preserve an explicitly
539
+ configured native value and otherwise make the existing key available
540
+ under OpenCode's spelling.
541
+ """
542
+ env = os.environ.copy()
543
+ env["LEMMING_PARENT_TASK_ID"] = task_id
544
+ env["LEMMING_PARENT_TASKS_FILE"] = str(tasks_file.resolve())
545
+
546
+ runner_base = os.path.basename(cmd[0]) if cmd else ""
547
+ if runner_base.startswith("opencode"):
548
+ google_api_key = env.get("GOOGLE_API_KEY") or env.get("GEMINI_API_KEY")
549
+ if google_api_key and not env.get("GOOGLE_GENERATIVE_AI_API_KEY"):
550
+ env["GOOGLE_GENERATIVE_AI_API_KEY"] = google_api_key
551
+
552
+ return env
553
+
554
+
496
555
  def run_with_heartbeat(
497
556
  cmd: list[str],
498
557
  tasks_file: pathlib.Path,
@@ -545,9 +604,7 @@ def run_with_heartbeat(
545
604
 
546
605
  # Start the process in a new session so we can kill its entire process
547
606
  # tree if needed.
548
- env = os.environ.copy()
549
- env["LEMMING_PARENT_TASK_ID"] = task_id
550
- env["LEMMING_PARENT_TASKS_FILE"] = str(tasks_file.resolve())
607
+ env = _runner_environment(cmd, tasks_file, task_id)
551
608
 
552
609
  process = subprocess.Popen(
553
610
  cmd,
@@ -80,11 +80,50 @@ def test_build_runner_command_time_limit_ignored_by_other_runners():
80
80
 
81
81
 
82
82
  def test_build_runner_command_aider():
83
+ """Explicit legacy Aider configurations remain usable."""
83
84
  cmd = runner.build_runner_command("aider", "my prompt", yolo=True)
84
85
  assert "--yes" in cmd
85
86
  assert "--message" in cmd
86
87
 
87
88
 
89
+ def test_build_runner_command_opencode():
90
+ cmd = runner.build_runner_command("opencode", "my prompt", yolo=True)
91
+ assert cmd == [
92
+ "opencode",
93
+ "run",
94
+ "--auto",
95
+ "--format",
96
+ "json",
97
+ "my prompt",
98
+ ]
99
+
100
+
101
+ def test_build_runner_command_opencode_without_yolo():
102
+ cmd = runner.build_runner_command("opencode", "my prompt", yolo=False)
103
+ assert cmd == ["opencode", "run", "--format", "json", "my prompt"]
104
+
105
+
106
+ def test_build_runner_command_opencode_without_defaults_still_uses_run():
107
+ cmd = runner.build_runner_command(
108
+ "opencode", "my prompt", yolo=True, no_defaults=True
109
+ )
110
+ assert cmd == ["opencode", "run", "my prompt"]
111
+
112
+
113
+ def test_build_runner_command_opencode_does_not_duplicate_explicit_run():
114
+ cmd = runner.build_runner_command(
115
+ "opencode run --pure", "my prompt", yolo=False
116
+ )
117
+ assert cmd == [
118
+ "opencode",
119
+ "run",
120
+ "--format",
121
+ "json",
122
+ "--pure",
123
+ "my prompt",
124
+ ]
125
+
126
+
88
127
  def test_build_runner_command_codex():
89
128
  cmd = runner.build_runner_command("codex", "my prompt", yolo=True)
90
129
  assert cmd == [
@@ -214,10 +253,13 @@ def test_conflicting_boolean_flag_does_not_consume_next_flag():
214
253
  def test_model_is_appended_for_known_runners():
215
254
  """A configured model reaches the runner as --model."""
216
255
  cmd = runner.build_runner_command(
217
- "agy", "prompt", yolo=True, model="gemini-3.6-flash-high"
256
+ "opencode",
257
+ "prompt",
258
+ yolo=True,
259
+ model="google/gemini-flash-latest",
218
260
  )
219
261
 
220
- assert cmd[cmd.index("--model") + 1] == "gemini-3.6-flash-high"
262
+ assert cmd[cmd.index("--model") + 1] == "google/gemini-flash-latest"
221
263
 
222
264
 
223
265
  def test_model_defers_to_explicit_runner_string_flag():
@@ -444,6 +486,42 @@ def test_run_with_heartbeat_log_header(tmp_path):
444
486
  assert "=" * 80 not in content_2
445
487
 
446
488
 
489
+ def test_opencode_environment_aliases_google_api_key(tmp_path, monkeypatch):
490
+ monkeypatch.setenv("GOOGLE_API_KEY", "existing-key")
491
+ monkeypatch.delenv("GOOGLE_GENERATIVE_AI_API_KEY", raising=False)
492
+
493
+ env = runner._runner_environment(
494
+ ["/usr/local/bin/opencode", "run"], tmp_path / "tasks.yml", "abc"
495
+ )
496
+
497
+ assert env["GOOGLE_GENERATIVE_AI_API_KEY"] == "existing-key"
498
+
499
+
500
+ def test_opencode_environment_preserves_native_google_key(
501
+ tmp_path, monkeypatch
502
+ ):
503
+ monkeypatch.setenv("GOOGLE_API_KEY", "legacy-key")
504
+ monkeypatch.setenv("GOOGLE_GENERATIVE_AI_API_KEY", "native-key")
505
+
506
+ env = runner._runner_environment(
507
+ ["opencode", "run"], tmp_path / "tasks.yml", "abc"
508
+ )
509
+
510
+ assert env["GOOGLE_GENERATIVE_AI_API_KEY"] == "native-key"
511
+
512
+
513
+ def test_opencode_environment_accepts_gemini_api_key(tmp_path, monkeypatch):
514
+ monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
515
+ monkeypatch.delenv("GOOGLE_GENERATIVE_AI_API_KEY", raising=False)
516
+ monkeypatch.setenv("GEMINI_API_KEY", "gemini-key")
517
+
518
+ env = runner._runner_environment(
519
+ ["opencode", "run"], tmp_path / "tasks.yml", "abc"
520
+ )
521
+
522
+ assert env["GOOGLE_GENERATIVE_AI_API_KEY"] == "gemini-key"
523
+
524
+
447
525
  def test_run_with_heartbeat_records_runner_and_hook_times(tmp_path):
448
526
  tasks_file = tmp_path / "tasks.yml"
449
527
  task_id = "timed_task"
@@ -814,6 +892,15 @@ def test_extract_error_message_from_turn_failed():
814
892
  assert runner.extract_error_message(output) == "quota exhausted"
815
893
 
816
894
 
895
+ def test_extract_error_message_from_opencode_error_event():
896
+ output = (
897
+ '{"type":"error","sessionID":"ses_123","error":'
898
+ '{"name":"ProviderError","data":{"message":"quota exhausted"}}}\n'
899
+ )
900
+
901
+ assert runner.extract_error_message(output) == "quota exhausted"
902
+
903
+
817
904
  def test_extract_error_message_ignores_tool_failures():
818
905
  """A failed shell command inside the agent is not the runner failing."""
819
906
  output = (
@@ -917,6 +1004,15 @@ _AGY_FINAL = (
917
1004
  '{"event":"result","result":{"status":"SUCCESS",'
918
1005
  '"response":"All tests pass.\\n","num_turns":1}}\n'
919
1006
  )
1007
+ _OPENCODE_FINAL = (
1008
+ '{"type":"step_start","sessionID":"ses_123","part":'
1009
+ '{"type":"step-start"}}\n'
1010
+ '{"type":"text","sessionID":"ses_123","part":'
1011
+ '{"type":"text","text":"All tests pass.",'
1012
+ '"time":{"start":1,"end":2}}}\n'
1013
+ '{"type":"step_finish","sessionID":"ses_123","part":'
1014
+ '{"type":"step-finish"}}\n'
1015
+ )
920
1016
 
921
1017
 
922
1018
  def test_extract_final_message_from_claude_result_event():
@@ -934,6 +1030,11 @@ def test_extract_final_message_from_agy_result_event():
934
1030
  assert runner.extract_final_message(_AGY_FINAL) == "All tests pass."
935
1031
 
936
1032
 
1033
+ def test_extract_final_message_from_opencode_text_event():
1034
+ """OpenCode reports completed assistant text in a text-part event."""
1035
+ assert runner.extract_final_message(_OPENCODE_FINAL) == "All tests pass."
1036
+
1037
+
937
1038
  def test_extract_final_message_skips_non_json_preamble():
938
1039
  """Codex prints a plain-text notice before its first event."""
939
1040
  output = "Reading additional input from stdin...\n" + _CODEX_FINAL
@@ -55,7 +55,7 @@ test.describe('Dashboard E2E', () => {
55
55
  } else if (url.includes('/api/runners')) {
56
56
  await route.fulfill({
57
57
  contentType: 'application/json',
58
- json: ['agy', 'aider'],
58
+ json: ['agy', 'opencode'],
59
59
  });
60
60
  } else if (url.includes('/api/hooks')) {
61
61
  await route.fulfill({
@@ -47,7 +47,7 @@ test.describe('Favicon Status Synchronization', () => {
47
47
  await page.route('**/api/runners', async (route) => {
48
48
  await route.fulfill({
49
49
  contentType: 'application/json',
50
- json: ['agy', 'aider'],
50
+ json: ['agy', 'opencode'],
51
51
  });
52
52
  });
53
53
 
@@ -58,7 +58,7 @@ const mockTasks = [
58
58
  attempts: 0,
59
59
  has_runner_log: false,
60
60
  pid: null,
61
- runner: 'aider',
61
+ runner: 'opencode',
62
62
  parent: 'd4e5f6',
63
63
  completed_at: null,
64
64
  run_time: null,
@@ -200,7 +200,7 @@ test.describe('Screenshot Generation', () => {
200
200
  await page.route('**/api/runners', async (route) => {
201
201
  await route.fulfill({
202
202
  contentType: 'application/json',
203
- json: ['agy', 'aider', 'claude', 'codex'],
203
+ json: ['agy', 'opencode', 'claude', 'codex'],
204
204
  });
205
205
  });
206
206
  await page.route('**/api/hooks', async (route) => {
@@ -222,7 +222,7 @@ wheels = [
222
222
 
223
223
  [[package]]
224
224
  name = "lemming-cli"
225
- version = "0.4.2"
225
+ version = "0.4.3"
226
226
  source = { editable = "." }
227
227
  dependencies = [
228
228
  { name = "beautifulsoup4" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes