execforge 0.1.0__py3-none-any.whl

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 (44) hide show
  1. execforge-0.1.0.dist-info/METADATA +367 -0
  2. execforge-0.1.0.dist-info/RECORD +44 -0
  3. execforge-0.1.0.dist-info/WHEEL +5 -0
  4. execforge-0.1.0.dist-info/entry_points.txt +5 -0
  5. execforge-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. execforge-0.1.0.dist-info/top_level.txt +1 -0
  7. orchestrator/__init__.py +4 -0
  8. orchestrator/__main__.py +5 -0
  9. orchestrator/backends/__init__.py +1 -0
  10. orchestrator/backends/base.py +29 -0
  11. orchestrator/backends/factory.py +53 -0
  12. orchestrator/backends/llm_cli_backend.py +87 -0
  13. orchestrator/backends/mock_backend.py +34 -0
  14. orchestrator/backends/shell_backend.py +49 -0
  15. orchestrator/cli/__init__.py +1 -0
  16. orchestrator/cli/main.py +971 -0
  17. orchestrator/config.py +272 -0
  18. orchestrator/domain/__init__.py +1 -0
  19. orchestrator/domain/types.py +77 -0
  20. orchestrator/exceptions.py +18 -0
  21. orchestrator/git/__init__.py +1 -0
  22. orchestrator/git/service.py +202 -0
  23. orchestrator/logging_setup.py +53 -0
  24. orchestrator/prompts/__init__.py +1 -0
  25. orchestrator/prompts/parser.py +91 -0
  26. orchestrator/reporting/__init__.py +1 -0
  27. orchestrator/reporting/console.py +197 -0
  28. orchestrator/reporting/events.py +44 -0
  29. orchestrator/reporting/selection_result.py +15 -0
  30. orchestrator/services/__init__.py +1 -0
  31. orchestrator/services/agent_runner.py +831 -0
  32. orchestrator/services/agent_service.py +122 -0
  33. orchestrator/services/project_service.py +47 -0
  34. orchestrator/services/prompt_source_service.py +65 -0
  35. orchestrator/services/run_service.py +42 -0
  36. orchestrator/services/step_executor.py +100 -0
  37. orchestrator/services/task_service.py +155 -0
  38. orchestrator/storage/__init__.py +1 -0
  39. orchestrator/storage/db.py +29 -0
  40. orchestrator/storage/models.py +95 -0
  41. orchestrator/utils/__init__.py +1 -0
  42. orchestrator/utils/process.py +44 -0
  43. orchestrator/validation/__init__.py +1 -0
  44. orchestrator/validation/pipeline.py +52 -0
@@ -0,0 +1,367 @@
1
+ Metadata-Version: 2.4
2
+ Name: execforge
3
+ Version: 0.1.0
4
+ Summary: Production-minded local CLI for autonomous repo orchestration
5
+ Author: Open Source Contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Matt
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.11
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: typer>=0.12.0
32
+ Requires-Dist: SQLAlchemy>=2.0.0
33
+ Requires-Dist: PyYAML>=6.0
34
+ Requires-Dist: platformdirs>=4.2.0
35
+ Requires-Dist: rich>=13.7.0
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=8.0; extra == "dev"
38
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # Execforge
42
+
43
+ `execforge` is a local CLI that takes tasks from one repo and applies them to another repo.
44
+
45
+ ## How to think about it
46
+
47
+ Keep this simple mental model:
48
+
49
+ - **Prompt source**: where tasks come from (a git repo with task files)
50
+ - **Project repo**: the codebase that gets changed
51
+ - **Agent**: the execution profile that connects the two and runs tasks
52
+
53
+ That is the whole product loop.
54
+
55
+ ## Install
56
+
57
+ Choose one:
58
+
59
+ ```bash
60
+ # recommended
61
+ pipx install execforge
62
+
63
+ # or
64
+ pip install execforge
65
+
66
+ # local dev
67
+ pip install -e .
68
+ ```
69
+
70
+ Install with Python virtualenv (local dev):
71
+
72
+ ```bash
73
+ # 1) create a virtual environment (Python 3.11+)
74
+ python -m venv .venv
75
+
76
+ # 2) activate it
77
+ # Windows (PowerShell)
78
+ .venv\Scripts\Activate.ps1
79
+ # Windows (cmd)
80
+ .venv\Scripts\activate.bat
81
+ # macOS/Linux
82
+ source .venv/bin/activate
83
+
84
+ # 3) upgrade packaging tools (recommended)
85
+ python -m pip install --upgrade pip setuptools wheel
86
+
87
+ # 4) install this project in editable mode
88
+ pip install -e .
89
+
90
+ # 5) verify CLI install
91
+ execforge --help
92
+ ```
93
+
94
+ Check install:
95
+
96
+ ```bash
97
+ execforge --help
98
+ ```
99
+
100
+ Aliases also work: `agent-orchestrator`, `orchestrator`, `agent-controlplane`.
101
+
102
+ ## Quick start (workflow-first)
103
+
104
+ ### 1) Initialize
105
+
106
+ ```bash
107
+ execforge init
108
+ ```
109
+
110
+ The init wizard guides setup for first prompt source, project repo, and agent.
111
+
112
+ ### 2) Connect a task source
113
+
114
+ ```bash
115
+ execforge prompt-source add prompts https://github.com/your-org/prompt-repo.git --branch main --folder-scope tasks
116
+ execforge prompt-source sync prompts
117
+ ```
118
+
119
+ If remote branch is missing and you want Execforge to create/push it:
120
+
121
+ ```bash
122
+ execforge prompt-source sync prompts --bootstrap-missing-branch
123
+ ```
124
+
125
+ ### 3) Connect a project repo
126
+
127
+ ```bash
128
+ execforge project add app ~/src/my-app
129
+ ```
130
+
131
+ ### 4) Create an agent
132
+
133
+ ```bash
134
+ execforge agent add app-agent prompts app --execution-backend multi
135
+
136
+ # ids also work if you prefer
137
+ execforge agent add app-agent 1 1 --execution-backend multi
138
+ ```
139
+
140
+ ### 5) Run the agent
141
+
142
+ One run:
143
+
144
+ ```bash
145
+ execforge agent run app-agent
146
+ ```
147
+
148
+ Continuous loop:
149
+
150
+ ```bash
151
+ execforge agent loop app-agent
152
+ ```
153
+
154
+ Loop defaults to new prompts only. If you want existing eligible tasks too:
155
+
156
+ ```bash
157
+ execforge agent loop app-agent --all-eligible-prompts
158
+ ```
159
+
160
+ ### 6) Inspect results
161
+
162
+ ```bash
163
+ execforge task list
164
+ execforge run list
165
+ execforge status
166
+ ```
167
+
168
+ ## Daily workflow
169
+
170
+ ```bash
171
+ execforge status
172
+ execforge prompt-source sync <source-name>
173
+ execforge agent run <agent-name>
174
+ execforge run list
175
+ ```
176
+
177
+ If you want continuous polling:
178
+
179
+ ```bash
180
+ execforge agent loop <agent-name>
181
+ ```
182
+
183
+ ## Backends (Claude/Codex/OpenCode/Shell)
184
+
185
+ Backends are interchangeable execution options for task steps.
186
+
187
+ - `shell` for explicit commands
188
+ - `claude`, `codex`, `opencode` when those CLIs are installed and enabled
189
+ - `mock` fallback backend for local/dev flows
190
+
191
+ Tasks can express preferences per step, and Execforge routes each step to an available backend.
192
+
193
+ ## Task format
194
+
195
+ Tasks can be Markdown with YAML frontmatter or pure YAML.
196
+
197
+ ```markdown
198
+ ---
199
+ id: quick-001
200
+ title: Create scaffold
201
+ status: todo
202
+ steps:
203
+ - id: create
204
+ type: shell
205
+ tool_preferences: [shell]
206
+ command: python -m pytest
207
+ - id: summarize
208
+ type: llm_summary
209
+ tool_preferences: [codex, claude, opencode, mock]
210
+ model: ollama/llama3.2
211
+ ---
212
+
213
+ Create project scaffold.
214
+ ```
215
+
216
+ For OpenCode steps, `model` maps to `--model <provider/model>`.
217
+
218
+ Optional task-level git overrides:
219
+
220
+ ```yaml
221
+ git:
222
+ base_branch: main
223
+ work_branch: agent/custom/quick-001
224
+ push_on_success: false
225
+ ```
226
+
227
+ Defaults (when omitted):
228
+
229
+ - `base_branch`: project repo default branch
230
+ - `work_branch`: `agent/<agent-name>/<task-id>`
231
+ - `push_on_success`: agent push policy
232
+
233
+ ## Managing configs
234
+
235
+ ### App config
236
+
237
+ ```bash
238
+ execforge config # defaults to config show
239
+ execforge config show
240
+ execforge config keys
241
+ execforge config set log_level DEBUG
242
+ execforge config set --set default_timeout_seconds=120 --set default_allow_push=true
243
+ execforge config reset default_timeout_seconds
244
+ execforge config reset --all
245
+ ```
246
+
247
+ Sensitive values are masked in `config show`.
248
+
249
+ ### Agent config
250
+
251
+ ```bash
252
+ execforge agent # defaults to agent list
253
+ execforge agent list # full JSON blocks
254
+ execforge agent update test-agent --set max_steps=40 --set push_policy=on-success
255
+ execforge agent update test-agent --set safety_settings.allow_push=true
256
+ execforge agent delete test-agent --yes
257
+ ```
258
+
259
+ ## Commands by workflow
260
+
261
+ ### Setup
262
+
263
+ - `execforge init`
264
+ - `execforge doctor`
265
+
266
+ ### Prompt sources (task origin)
267
+
268
+ - `execforge prompt-source add`
269
+ - `execforge prompt-source list`
270
+ - `execforge prompt-source sync`
271
+
272
+ ### Project repos (code targets)
273
+
274
+ - `execforge project add`
275
+ - `execforge project list`
276
+
277
+ ### Agents (execution profiles)
278
+
279
+ - `execforge agent add`
280
+ - `execforge agent list`
281
+ - `execforge agent list --compact`
282
+ - `execforge agent update`
283
+ - `execforge agent delete`
284
+ - `execforge agent run <agent-name-or-id>`
285
+ - `execforge agent loop <agent-name-or-id>`
286
+
287
+ ### Task and run inspection
288
+
289
+ - `execforge task list`
290
+ - `execforge task inspect <task-id>`
291
+ - `execforge task set-status <task-id> <status>`
292
+ - `execforge task retry <task-id>`
293
+ - `execforge run list`
294
+ - `execforge status`
295
+ - `execforge start`
296
+
297
+ ## When nothing runs
298
+
299
+ If a run ends with noop, Execforge prints the reason and next step.
300
+
301
+ Common reasons:
302
+
303
+ - no task files were discovered in the prompt source scope
304
+ - all tasks are already in the current only-new baseline
305
+ - all tasks are already complete
306
+ - tasks exist but none are currently actionable
307
+
308
+ Useful fixes:
309
+
310
+ ```bash
311
+ execforge prompt-source sync <source-name>
312
+ execforge task list
313
+ execforge agent loop <agent-name> --all-eligible-prompts
314
+ execforge agent loop <agent-name> --reset-only-new-baseline
315
+ ```
316
+
317
+ `--reset-only-new-baseline` applies to the first loop run, then loop returns to normal only-new behavior.
318
+
319
+ ## Where state is stored
320
+
321
+ Execforge keeps state outside your project repos.
322
+
323
+ Default location:
324
+
325
+ - Linux: `~/.local/share/agent-orchestrator/`
326
+ - macOS: `~/Library/Application Support/agent-orchestrator/`
327
+ - Windows: `%LOCALAPPDATA%\agent-orchestrator\agent-orchestrator\`
328
+
329
+ Override:
330
+
331
+ ```bash
332
+ export AGENT_ORCHESTRATOR_HOME=~/.agent-orchestrator
333
+ ```
334
+
335
+ ## More docs
336
+
337
+ - `docs/USAGE_WALKTHROUGH.md` - practical end-to-end flow
338
+ - `docs/ARCHITECTURE.md` - implementation layout
339
+
340
+ ## CI/CD and PyPI publish
341
+
342
+ This repo includes GitHub Actions pipelines:
343
+
344
+ - `.github/workflows/ci.yml` - lint, tests, package build, and `twine check`
345
+ - `.github/workflows/publish-testpypi.yml` - manual publish to TestPyPI
346
+ - `.github/workflows/publish-pypi.yml` - publish to PyPI on release (and manual dispatch)
347
+
348
+ Required repository secrets:
349
+
350
+ - `TEST_PYPI_API_TOKEN` for TestPyPI publishing
351
+ - `PYPI_API_TOKEN` for PyPI publishing
352
+
353
+ Typical release flow:
354
+
355
+ ```bash
356
+ # 1) bump version in pyproject.toml
357
+ # 2) commit and tag
358
+ git tag v0.1.1
359
+ git push origin main --tags
360
+
361
+ # 3) create/publish a GitHub Release for that tag
362
+ # -> triggers publish-pypi.yml
363
+ ```
364
+
365
+ ## License
366
+
367
+ MIT (see `LICENSE`).
@@ -0,0 +1,44 @@
1
+ execforge-0.1.0.dist-info/licenses/LICENSE,sha256=cvc_iNTDxqu0RnxCLH1kfw5ZN03VGI-mCTNn8YSv298,1061
2
+ orchestrator/__init__.py,sha256=sVTlg8Mreo_-UsTaq3FkULJqoo_lQTRnte8MlRVnbS4,82
3
+ orchestrator/__main__.py,sha256=0mzzNd99pzV8Lre8wuEYl2sJ1nkssz41AMfehCOEqP0,77
4
+ orchestrator/config.py,sha256=eWCk_GFyMBewvsU-2gyWSs9t3mRYILzDgODabPO4Rko,8288
5
+ orchestrator/exceptions.py,sha256=RVSFX35PNDt48f4Wfph0aSddxDks1dAzWFTAa6pdNiE,474
6
+ orchestrator/logging_setup.py,sha256=OG1tKygZ14-xI6-LImoaIJ2nAD-pckJ73H8c4NyfWlk,1588
7
+ orchestrator/backends/__init__.py,sha256=a2z3ZihR1jKmZAOey9NGAR0J05fiHy4oiOoyImAcV7w,41
8
+ orchestrator/backends/base.py,sha256=M3JAe-zzigSpO2R45bQQfpYY9etJ_hckwpwNl3TMyao,714
9
+ orchestrator/backends/factory.py,sha256=D6SabjN-MvvrxKXfW7T4-OeU8q6X1oiWIkCJtO-bxrM,2334
10
+ orchestrator/backends/llm_cli_backend.py,sha256=i5YNSiRjY4DRLVaby_OQgGiO3QYyFypsOlZ16fXr4tM,3433
11
+ orchestrator/backends/mock_backend.py,sha256=6z5txdyZfqUK-aLV2KpG6l2UUWYJb9nzp86M9eDhLL0,1186
12
+ orchestrator/backends/shell_backend.py,sha256=8W_am3a-KsLe1bUJ7O5lSes8CWGCts2DkNbEeEaa-0Q,2004
13
+ orchestrator/cli/__init__.py,sha256=C63yWifzpA0IV7YWDatpAdrhoV8zjqxAKv0xMf09VdM,19
14
+ orchestrator/cli/main.py,sha256=cLJYQfhIXnC6aDG_cb8028hvIdT-oUlAivMIR7e6A48,35253
15
+ orchestrator/domain/__init__.py,sha256=aEZTR2LNKnB0Q_VRUoh4kzbnL5CMWHXq82uDu0rTY9E,38
16
+ orchestrator/domain/types.py,sha256=TKix6w8H9lYwDPvmOk0kFm3Kbkf6XyO6w3L8oJvFLWo,1864
17
+ orchestrator/git/__init__.py,sha256=tcVcTMg1slpazqrZbSsWk0L7UrjnZ861j_t0lONu_5E,32
18
+ orchestrator/git/service.py,sha256=GiBlH4DVZTeY-UVLjYrIMgvcqVF5PgzbPsWbAPmIXts,9409
19
+ orchestrator/prompts/__init__.py,sha256=EZ1sIIbFt6qlcow-dw89fxsRdCNlwW9AEKetDwQuUQY,39
20
+ orchestrator/prompts/parser.py,sha256=bRrza8RMdeoHrEaa34z2S_munsJsjC1KUhvRj_1MvvA,3160
21
+ orchestrator/reporting/__init__.py,sha256=9wpbZKzl2pdXG7C24Cbxm3vvE0jrBK4QPB9gfxzKrjo,57
22
+ orchestrator/reporting/console.py,sha256=q6ihIXj9oYfvAgq3n_jJItlzZAI6PYWxB1l1uKl1-P8,8374
23
+ orchestrator/reporting/events.py,sha256=hbqFFFVxVeIU0yU4Lzikfe_c7RP63RNocbUzP8F9tT0,1272
24
+ orchestrator/reporting/selection_result.py,sha256=Qf_UMHKFrR5IXv6NN5HZDGuBESUdXjPTowvxV1XHB0Q,344
25
+ orchestrator/services/__init__.py,sha256=Sz5tMokf5S97Gjyb7eiPsqTJXf23yyRfsAzhMcX3PAw,33
26
+ orchestrator/services/agent_runner.py,sha256=dop7r1C0OhYsh3wmFeiB9t3PzWP4909x7_6zFO3-nCg,32749
27
+ orchestrator/services/agent_service.py,sha256=rZgUqU1mO8YYcMF2Sz95Xwl5Z3kbNEOA2xrAAgzWUpE,4916
28
+ orchestrator/services/project_service.py,sha256=lT3KvjtwaeMW8dqnRW-MWznxhv3DcurUrn6s7u24QBM,1476
29
+ orchestrator/services/prompt_source_service.py,sha256=OEgdl2xirSTX6A6OeVIVe-vZRgYcgSvQ-ndKuwkUVK8,2222
30
+ orchestrator/services/run_service.py,sha256=Z_HMO22sBrXemVpLGdcSkyYw4nw_--AReA1x8Mr6F9M,1385
31
+ orchestrator/services/step_executor.py,sha256=OFrioHEfrOZYbxNyAAVIFRMLRkbRIWI9WuVa2zqQ7oA,4153
32
+ orchestrator/services/task_service.py,sha256=rcZNBZPsZ6991-S7TeAoqL6NL_NloSI4P_jxUC-bRfs,6003
33
+ orchestrator/storage/__init__.py,sha256=kCNaKkkO_VlrVA7STduJRORDeFWeQo4c-6RpKrV2mqA,36
34
+ orchestrator/storage/db.py,sha256=94QH32-27ETcBqxr5EbHiIe_7Ze2vO2GpWMGhj7lbdU,596
35
+ orchestrator/storage/models.py,sha256=xVjfcve82o7eiaBnitgdbm1dEJx56rYnp9XfagwREwg,5134
36
+ orchestrator/utils/__init__.py,sha256=vTzpzAhwZwhp9xdzSN95SWmf02qgw7Asc0ULXG4aEok,23
37
+ orchestrator/utils/process.py,sha256=r4VAf3j5wQXmfOZxDE5qgDbNFoCa6HKZK2Hy2DSv6QE,1346
38
+ orchestrator/validation/__init__.py,sha256=tkPEbS5j_o_LJ-ysUg3odTtSQl1MUA3o9Tc5YbFaA-U,35
39
+ orchestrator/validation/pipeline.py,sha256=45M6c8G1vhJNlvF8x4tTFxwK2FLOwhoK4GCjEaThiqQ,2203
40
+ execforge-0.1.0.dist-info/METADATA,sha256=4jaYban7pMlceaNcKrj1lsai0CE-768iOULj-3fvFmM,8686
41
+ execforge-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
42
+ execforge-0.1.0.dist-info/entry_points.txt,sha256=TyH7qOa2W18eODIdQQ2bn4kXI7i8f8ESb2791SeTtYQ,195
43
+ execforge-0.1.0.dist-info/top_level.txt,sha256=xxQ-0cX7ZiHS5jT83NoSu1MB43Iw3IGXRlLTZrI3QX4,13
44
+ execforge-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ agent-controlplane = orchestrator.cli.main:main
3
+ agent-orchestrator = orchestrator.cli.main:main
4
+ execforge = orchestrator.cli.main:main
5
+ orchestrator = orchestrator.cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ orchestrator
@@ -0,0 +1,4 @@
1
+ """Repo orchestrator package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from orchestrator.cli.main import app
2
+
3
+
4
+ if __name__ == "__main__":
5
+ app()
@@ -0,0 +1 @@
1
+ """Execution backend implementations."""
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+
6
+ from orchestrator.domain.types import BackendContext, BackendResult, TaskStep
7
+ from orchestrator.storage.models import TaskORM
8
+
9
+
10
+ class ExecutionBackend(ABC):
11
+ name: str
12
+ supported_step_types: set[str]
13
+
14
+ def supports(self, step: TaskStep) -> bool:
15
+ return step.type in self.supported_step_types
16
+
17
+ def is_available(self) -> bool:
18
+ return True
19
+
20
+ @abstractmethod
21
+ def execute_step(
22
+ self,
23
+ step: TaskStep,
24
+ task: TaskORM,
25
+ project_path: Path,
26
+ prompt_root: Path,
27
+ context: BackendContext,
28
+ ) -> BackendResult:
29
+ raise NotImplementedError
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from orchestrator.backends.base import ExecutionBackend
6
+ from orchestrator.backends.llm_cli_backend import LlmCliBackend
7
+ from orchestrator.backends.mock_backend import MockBackend
8
+ from orchestrator.backends.shell_backend import ShellBackend
9
+ from orchestrator.exceptions import ConfigError
10
+ from orchestrator.storage.models import AgentORM
11
+
12
+
13
+ def build_backend_registry(agent: AgentORM) -> dict[str, ExecutionBackend]:
14
+ settings = json.loads(agent.model_settings_json or "{}")
15
+ safety = json.loads(agent.safety_settings_json or "{}")
16
+ backends_cfg = settings.get("backends", {})
17
+
18
+ registry: dict[str, ExecutionBackend] = {}
19
+
20
+ shell_cfg = backends_cfg.get("shell", {})
21
+ if agent.execution_backend == "shell" or shell_cfg.get("enabled", True):
22
+ command_template = settings.get("command_template") or shell_cfg.get("command_template")
23
+ allowed = shell_cfg.get("allowed_commands", safety.get("allowed_commands", []))
24
+ registry["shell"] = ShellBackend(command_template=command_template, allowed_commands=allowed)
25
+
26
+ for tool, defaults in {
27
+ "claude": {"binary": "claude", "args": ["-p"], "model_arg_name": None},
28
+ "codex": {"binary": "codex", "args": ["exec", "--prompt"], "model_arg_name": None},
29
+ "opencode": {"binary": "opencode", "args": ["run"], "model_arg_name": "--model"},
30
+ }.items():
31
+ cfg = backends_cfg.get(tool, {})
32
+ if not cfg.get("enabled", False):
33
+ continue
34
+ registry[tool] = LlmCliBackend(
35
+ name=tool,
36
+ binary=cfg.get("binary", defaults["binary"]),
37
+ args=list(cfg.get("args", defaults["args"])),
38
+ prompt_arg_template=cfg.get("prompt_arg_template", "{prompt}"),
39
+ requires_binary=bool(cfg.get("requires_binary", True)),
40
+ model_arg_name=cfg.get("model_arg_name", defaults["model_arg_name"]),
41
+ )
42
+
43
+ registry["mock"] = MockBackend()
44
+
45
+ if not registry:
46
+ raise ConfigError("No execution backends are enabled for this agent")
47
+ return registry
48
+
49
+
50
+ def default_backend_priority(agent: AgentORM) -> list[str]:
51
+ settings = json.loads(agent.model_settings_json or "{}")
52
+ priority = list(settings.get("backend_priority", []) or [])
53
+ return priority or ["codex", "claude", "opencode", "shell", "mock"]
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import shlex
5
+ import shutil
6
+
7
+ from orchestrator.backends.base import ExecutionBackend
8
+ from orchestrator.domain.types import BackendContext, BackendResult, TaskStep
9
+ from orchestrator.exceptions import BackendError
10
+ from orchestrator.storage.models import TaskORM
11
+ from orchestrator.utils.process import run_command
12
+
13
+
14
+ class LlmCliBackend(ExecutionBackend):
15
+ supported_step_types = {"llm_plan", "code_edit", "llm_summary"}
16
+
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ binary: str,
21
+ args: list[str] | None = None,
22
+ prompt_arg_template: str = "{prompt}",
23
+ requires_binary: bool = True,
24
+ model_arg_name: str | None = None,
25
+ ):
26
+ self.name = name
27
+ self.binary = binary
28
+ self.args = list(args or [])
29
+ self.prompt_arg_template = prompt_arg_template
30
+ self.requires_binary = requires_binary
31
+ self.model_arg_name = model_arg_name
32
+
33
+ def execute_step(
34
+ self,
35
+ step: TaskStep,
36
+ task: TaskORM,
37
+ project_path: Path,
38
+ prompt_root: Path,
39
+ context: BackendContext,
40
+ ) -> BackendResult:
41
+ if not self.is_available():
42
+ raise BackendError(f"Backend '{self.name}' unavailable: executable '{self.binary}' not found in PATH")
43
+
44
+ prompt = self._resolve_prompt(step, task, prompt_root)
45
+ cmd = [self.binary, *self.args]
46
+
47
+ # Optional per-step model override: step.metadata["model"] = "provider/model"
48
+ # Example: model: ollama/llama3.2
49
+ step_model = step.metadata.get("model") if isinstance(step.metadata, dict) else None
50
+ if step_model and self.model_arg_name and not self._has_arg(self.model_arg_name):
51
+ cmd.extend([self.model_arg_name, str(step_model)])
52
+
53
+ cmd.append(self.prompt_arg_template.format(prompt=prompt))
54
+ result = run_command(cmd, cwd=project_path, timeout=context.timeout_seconds)
55
+ summary = f"{self.name} exited with code {result.code}"
56
+ if result.code == 127:
57
+ summary = (
58
+ f"{self.name} executable not found. "
59
+ f"Install '{self.binary}', disable this backend, or enable mock fallback for the agent"
60
+ )
61
+ return BackendResult(
62
+ success=result.code == 0,
63
+ summary=summary,
64
+ stdout=result.stdout,
65
+ stderr=result.stderr,
66
+ tool_invocations=[{"tool": self.name, "step": step.id, "command": shlex.join(cmd), "exit_code": result.code}],
67
+ )
68
+
69
+ def is_available(self) -> bool:
70
+ if not self.requires_binary:
71
+ return True
72
+ return shutil.which(self.binary) is not None
73
+
74
+ def _resolve_prompt(self, step: TaskStep, task: TaskORM, prompt_root: Path) -> str:
75
+ if step.prompt_inline:
76
+ return step.prompt_inline
77
+ if step.prompt_file:
78
+ task_file_path = prompt_root / task.source_path
79
+ candidates = [task_file_path.parent / step.prompt_file, prompt_root / step.prompt_file]
80
+ for candidate in candidates:
81
+ if candidate.exists():
82
+ return candidate.read_text(encoding="utf-8")
83
+ raise BackendError(f"Prompt file not found for step '{step.id}': {step.prompt_file}")
84
+ return task.description
85
+
86
+ def _has_arg(self, flag: str) -> bool:
87
+ return any(arg == flag or arg.startswith(f"{flag}=") for arg in self.args)
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from orchestrator.backends.base import ExecutionBackend
6
+ from orchestrator.domain.types import BackendContext, BackendResult, TaskStep
7
+ from orchestrator.storage.models import TaskORM
8
+
9
+
10
+ class MockBackend(ExecutionBackend):
11
+ name = "mock"
12
+ supported_step_types = {"llm_plan", "llm_summary", "code_edit", "shell"}
13
+
14
+ def execute_step(
15
+ self,
16
+ step: TaskStep,
17
+ task: TaskORM,
18
+ project_path: Path,
19
+ prompt_root: Path,
20
+ context: BackendContext,
21
+ ) -> BackendResult:
22
+ marker = project_path / ".orchestrator"
23
+ marker.mkdir(parents=True, exist_ok=True)
24
+ task_ref = task.external_id or f"task-{task.id}"
25
+ out_file = marker / f"{task_ref}-{step.id}.txt"
26
+ out_file.write_text(
27
+ f"Completed by mock backend for {task_ref}:{step.id} ({step.type})\n\n{task.description}\n",
28
+ encoding="utf-8",
29
+ )
30
+ return BackendResult(
31
+ success=True,
32
+ summary=f"Mock backend completed step {step.id}",
33
+ tool_invocations=[{"tool": "mock_backend", "step": step.id, "output": str(out_file)}],
34
+ )