sisyphusfy 0.1.0__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.
- sisyphusfy-0.1.0/LICENSE +21 -0
- sisyphusfy-0.1.0/PKG-INFO +10 -0
- sisyphusfy-0.1.0/README.md +331 -0
- sisyphusfy-0.1.0/pyproject.toml +25 -0
- sisyphusfy-0.1.0/setup.cfg +4 -0
- sisyphusfy-0.1.0/src/sisyphusfy/__init__.py +6 -0
- sisyphusfy-0.1.0/src/sisyphusfy/adapters.py +221 -0
- sisyphusfy-0.1.0/src/sisyphusfy/cli.py +637 -0
- sisyphusfy-0.1.0/src/sisyphusfy/config.py +234 -0
- sisyphusfy-0.1.0/src/sisyphusfy/distribution.py +157 -0
- sisyphusfy-0.1.0/src/sisyphusfy/hooks.py +210 -0
- sisyphusfy-0.1.0/src/sisyphusfy/human.py +489 -0
- sisyphusfy-0.1.0/src/sisyphusfy/loop.py +587 -0
- sisyphusfy-0.1.0/src/sisyphusfy/result.py +45 -0
- sisyphusfy-0.1.0/src/sisyphusfy/runner.py +95 -0
- sisyphusfy-0.1.0/src/sisyphusfy/workflows.py +360 -0
- sisyphusfy-0.1.0/src/sisyphusfy.egg-info/PKG-INFO +10 -0
- sisyphusfy-0.1.0/src/sisyphusfy.egg-info/SOURCES.txt +58 -0
- sisyphusfy-0.1.0/src/sisyphusfy.egg-info/dependency_links.txt +1 -0
- sisyphusfy-0.1.0/src/sisyphusfy.egg-info/entry_points.txt +2 -0
- sisyphusfy-0.1.0/src/sisyphusfy.egg-info/requires.txt +4 -0
- sisyphusfy-0.1.0/src/sisyphusfy.egg-info/top_level.txt +1 -0
- sisyphusfy-0.1.0/tests/test_adapter_contracts.py +159 -0
- sisyphusfy-0.1.0/tests/test_adapters.py +197 -0
- sisyphusfy-0.1.0/tests/test_audit_gaps.py +238 -0
- sisyphusfy-0.1.0/tests/test_audit_workflow.py +600 -0
- sisyphusfy-0.1.0/tests/test_cli.py +274 -0
- sisyphusfy-0.1.0/tests/test_commit_staging.py +134 -0
- sisyphusfy-0.1.0/tests/test_config.py +182 -0
- sisyphusfy-0.1.0/tests/test_context.py +151 -0
- sisyphusfy-0.1.0/tests/test_distribution.py +194 -0
- sisyphusfy-0.1.0/tests/test_documentation_contracts.py +157 -0
- sisyphusfy-0.1.0/tests/test_documented_links.py +64 -0
- sisyphusfy-0.1.0/tests/test_dryrun.py +80 -0
- sisyphusfy-0.1.0/tests/test_dryrun_boundaries.py +177 -0
- sisyphusfy-0.1.0/tests/test_fallback_command.py +161 -0
- sisyphusfy-0.1.0/tests/test_hooks.py +311 -0
- sisyphusfy-0.1.0/tests/test_human.py +261 -0
- sisyphusfy-0.1.0/tests/test_loop_cli.py +156 -0
- sisyphusfy-0.1.0/tests/test_loop_completion.py +136 -0
- sisyphusfy-0.1.0/tests/test_loop_dryrun.py +187 -0
- sisyphusfy-0.1.0/tests/test_loop_fresh_process.py +117 -0
- sisyphusfy-0.1.0/tests/test_loop_results.py +130 -0
- sisyphusfy-0.1.0/tests/test_loop_state.py +145 -0
- sisyphusfy-0.1.0/tests/test_loop_stopping.py +157 -0
- sisyphusfy-0.1.0/tests/test_loop_verification.py +161 -0
- sisyphusfy-0.1.0/tests/test_model_fallback.py +172 -0
- sisyphusfy-0.1.0/tests/test_npm_launcher.py +128 -0
- sisyphusfy-0.1.0/tests/test_posix_installer.py +83 -0
- sisyphusfy-0.1.0/tests/test_powershell_installer.py +124 -0
- sisyphusfy-0.1.0/tests/test_process.py +147 -0
- sisyphusfy-0.1.0/tests/test_project_directory.py +205 -0
- sisyphusfy-0.1.0/tests/test_release_quality.py +54 -0
- sisyphusfy-0.1.0/tests/test_release_targets.py +179 -0
- sisyphusfy-0.1.0/tests/test_spec_validation.py +53 -0
- sisyphusfy-0.1.0/tests/test_unit.py +86 -0
- sisyphusfy-0.1.0/tests/test_verification_ordering.py +231 -0
- sisyphusfy-0.1.0/tests/test_version_dryrun.py +83 -0
- sisyphusfy-0.1.0/tests/test_version_sync.py +82 -0
- sisyphusfy-0.1.0/tests/test_workflow_adapters.py +356 -0
sisyphusfy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sisyphusfy contributors
|
|
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,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sisyphusfy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Project-agnostic supervisor for disposable AI-agent sessions
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
9
|
+
Requires-Dist: pyyaml>=6.0; extra == "dev"
|
|
10
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Sisyphusfy
|
|
2
|
+
|
|
3
|
+
Fresh agent sessions. Persistent project progress.
|
|
4
|
+
|
|
5
|
+
Sisyphusfy is a project-agnostic runner for executing software tasks through disposable AI-agent sessions. It keeps durable state in project files, supports multiple agent CLIs and model fallback, and avoids carrying an expensive conversation history from one task or specification to the next.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
AI coding sessions become expensive and fragile when conversation history grows across unrelated specifications. Sisyphusfy treats the workspace as the source of truth:
|
|
10
|
+
|
|
11
|
+
- one session performs a small unit of work;
|
|
12
|
+
- task state and a concise handoff are written to disk;
|
|
13
|
+
- failures, quota exhaustion, and timeouts can switch to another model;
|
|
14
|
+
- the next session reconstructs only the necessary context;
|
|
15
|
+
- a new specification starts with a fresh conversation.
|
|
16
|
+
|
|
17
|
+
## Design principles
|
|
18
|
+
|
|
19
|
+
1. The runner is not tied to a language, framework, repository layout, or OpenSpec.
|
|
20
|
+
2. Agent CLIs are adapters, not dependencies of the loop engine.
|
|
21
|
+
3. Task state is machine-checkable; handoff text is compact recovery context.
|
|
22
|
+
4. Verification is performed by the runner, not merely claimed by the agent.
|
|
23
|
+
5. Permission prompts and ambiguous choices stop safely instead of hanging forever.
|
|
24
|
+
6. Commit, archive, and push are opt-in operations.
|
|
25
|
+
7. Fresh context is the default; conversation continuation is explicit.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
**Repository**: https://github.com/lileililiwen/sisyphusfy
|
|
30
|
+
**Website**: https://sisyphusfy.dev/
|
|
31
|
+
**Releases**: https://github.com/lileililiwen/sisyphusfy/releases
|
|
32
|
+
|
|
33
|
+
### pip / pipx
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install sisyphusfy
|
|
37
|
+
# or
|
|
38
|
+
pipx install sisyphusfy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### npm
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g sisyphusfy
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### POSIX installer
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
curl -fsSL https://raw.githubusercontent.com/lileililiwen/sisyphusfy/main/install.sh | sh
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### PowerShell installer (Windows)
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
irm https://raw.githubusercontent.com/lileililiwen/sisyphusfy/main/install.ps1 | iex
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### From a checkout
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -e .
|
|
63
|
+
sisyphusfy --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The package requires Python 3.10 or newer. OpenCode, CodeBuddy, and any
|
|
67
|
+
project-specific verification tools are installed separately.
|
|
68
|
+
|
|
69
|
+
## Quick start: one invocation
|
|
70
|
+
|
|
71
|
+
Run a command with timeout:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
sisyphusfy -t 30 -- echo "hello world"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Dry-run (shows command without executing):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
sisyphusfy --dry-run -- echo "hello world"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
JSON output for automation:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
sisyphusfy --json -- python3 script.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Deliver a prompt via stdin:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
sisyphusfy -p "explain this repository" -- opencode run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Set working directory and environment variables:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
sisyphusfy -d /path/to/project -t 60 -e API_KEY=abc123 -e DEBUG=1 -- my-agent
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Quick start: human-friendly workflow
|
|
102
|
+
|
|
103
|
+
The high-level commands provide a simpler interface for common workflows.
|
|
104
|
+
They automatically discover tasks, configuration, and agent settings.
|
|
105
|
+
|
|
106
|
+
Initialize a project:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
sisyphusfy init
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Run an OpenSpec change:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
sisyphusfy run my-change
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Resume from durable state:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
sisyphusfy resume
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Check project status:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
sisyphusfy status
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Diagnose configuration issues:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
sisyphusfy doctor
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Dry-run to preview planned actions:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
sisyphusfy run my-change --dry-run
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use JSON output for automation:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
sisyphusfy status --json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Configuration precedence: CLI flags > `.sisyphusfy.toml` > user defaults > built-in defaults.
|
|
149
|
+
|
|
150
|
+
The selected project directory (`--project-dir`, default `.`) is the working
|
|
151
|
+
directory for every subprocess: agent, verification, workflow validation, and
|
|
152
|
+
completion hooks. Relative paths resolve against it.
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
| Document | Contents |
|
|
157
|
+
|----------|----------|
|
|
158
|
+
| [docs/configuration.md](docs/configuration.md) | Every `.sisyphusfy.toml` field: type, default, precedence, safety |
|
|
159
|
+
| [docs/adapters.md](docs/adapters.md) | `AgentAdapter` protocol, registry, command construction, model fallback, failure classification, workflow adapters |
|
|
160
|
+
| [docs/examples.md](docs/examples.md) | Minimal end-to-end setups for Python, Rust, JavaScript, Flutter, and .NET |
|
|
161
|
+
| [docs/ci.md](docs/ci.md) | Continuous-integration usage and JSON result consumption |
|
|
162
|
+
| [docs/security.md](docs/security.md) | Command-execution model and review checklist |
|
|
163
|
+
| [docs/deployment.md](docs/deployment.md) | GitHub Pages and custom domain setup |
|
|
164
|
+
|
|
165
|
+
## Quick start: durable loop
|
|
166
|
+
|
|
167
|
+
The loop starts a fresh agent process for every iteration. The task file and
|
|
168
|
+
handoff file are the durable state; previous agent conversation history is not
|
|
169
|
+
restored.
|
|
170
|
+
|
|
171
|
+
For a Markdown checklist:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
sisyphusfy loop \
|
|
175
|
+
--task-path TASKS.md \
|
|
176
|
+
--handoff-path HANDOFF.md \
|
|
177
|
+
--completion-strategy markdown \
|
|
178
|
+
--adapter opencode \
|
|
179
|
+
--model-chain mimo-v2.5 minimax-3 \
|
|
180
|
+
--agent-timeout 2700 \
|
|
181
|
+
--max-iterations 20 \
|
|
182
|
+
--verification-command pytest
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Use CodeBuddy instead:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
sisyphusfy loop \
|
|
189
|
+
--task-path TASKS.md \
|
|
190
|
+
--handoff-path HANDOFF.md \
|
|
191
|
+
--completion-strategy markdown \
|
|
192
|
+
--adapter codebuddy \
|
|
193
|
+
--model-chain minimax-3
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
For an OpenSpec change, point the workflow adapter at the change directory:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
sisyphusfy loop \
|
|
200
|
+
--task-path openspec/changes/my-change/tasks.md \
|
|
201
|
+
--handoff-path HANDOFF.md \
|
|
202
|
+
--workflow-type openspec \
|
|
203
|
+
--workflow-change-dir openspec/changes/my-change \
|
|
204
|
+
--workflow-validation-command openspec validate my-change --strict \
|
|
205
|
+
--adapter opencode \
|
|
206
|
+
--model-chain mimo-v2.5 minimax-3
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The default prompt is intentionally short. It asks the agent to read the
|
|
210
|
+
configured task and handoff files, implement one task, update state, and stop
|
|
211
|
+
when blocked. Override it with `--prompt-template` when a project needs a
|
|
212
|
+
different instruction contract.
|
|
213
|
+
|
|
214
|
+
## Dry-run and machine output
|
|
215
|
+
|
|
216
|
+
Preview a single command without executing it:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
sisyphusfy --dry-run -- echo "hello world"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Preview the loop and hooks:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
sisyphusfy loop --dry-run \
|
|
226
|
+
--task-path TASKS.md \
|
|
227
|
+
--completion-strategy markdown \
|
|
228
|
+
--adapter opencode \
|
|
229
|
+
--model-chain mimo-v2.5
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Use `--json` for automation and CI consumers.
|
|
233
|
+
|
|
234
|
+
## Optional completion hooks
|
|
235
|
+
|
|
236
|
+
Archive and commit hooks are disabled by default. Enable them only after
|
|
237
|
+
verification, and always provide an explicit commit allowlist:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
sisyphusfy loop \
|
|
241
|
+
--task-path openspec/changes/my-change/tasks.md \
|
|
242
|
+
--workflow-type openspec \
|
|
243
|
+
--workflow-change-dir openspec/changes/my-change \
|
|
244
|
+
--workflow-validation-command openspec validate my-change --strict \
|
|
245
|
+
--adapter opencode \
|
|
246
|
+
--archive-command openspec archive my-change -y \
|
|
247
|
+
--commit-command git commit -m "feat: complete my change" \
|
|
248
|
+
--commit-allowed-files src README.md
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Review the dry-run output before enabling these hooks. Sisyphusfy does not
|
|
252
|
+
provide automatic push support.
|
|
253
|
+
|
|
254
|
+
## Programmatic usage
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from sisyphusfy.runner import run_agent
|
|
258
|
+
from sisyphusfy.result import Classification
|
|
259
|
+
|
|
260
|
+
result = run_agent(
|
|
261
|
+
["python3", "script.py"],
|
|
262
|
+
working_directory="/path/to/project",
|
|
263
|
+
prompt="do the thing",
|
|
264
|
+
timeout=30.0,
|
|
265
|
+
env={"API_KEY": "abc123"},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if result.classification == Classification.SUCCESS:
|
|
269
|
+
print("Done:", result.stdout)
|
|
270
|
+
else:
|
|
271
|
+
print("Failed:", result.classification.value)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Development
|
|
275
|
+
|
|
276
|
+
Development is tracked with OpenSpec. See `openspec/changes/` for active work and `openspec/specs/` for accepted capability specifications.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
openspec list
|
|
280
|
+
openspec list
|
|
281
|
+
openspec validate --all --strict
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The repository is initialized for both OpenCode and CodeBuddy workflows. Those integrations are development conveniences; the product itself remains agent-neutral.
|
|
285
|
+
|
|
286
|
+
## Supported release targets
|
|
287
|
+
|
|
288
|
+
Installers download a prebuilt archive that matches the host platform and
|
|
289
|
+
architecture. The published set is the same set CI builds, and every target is
|
|
290
|
+
built on a runner whose native architecture matches the artifact label.
|
|
291
|
+
|
|
292
|
+
| Platform | Architecture | CI runner |
|
|
293
|
+
|----------|--------------|-----------|
|
|
294
|
+
| linux | x86_64 | `ubuntu-latest` |
|
|
295
|
+
| darwin | x86_64 | `macos-15-intel` |
|
|
296
|
+
| darwin | aarch64 | `macos-14` |
|
|
297
|
+
| win32 | x86_64 | `windows-latest` |
|
|
298
|
+
|
|
299
|
+
`linux/aarch64` is deferred: it is not published because no runner in the
|
|
300
|
+
current matrix produces it natively. Install with `pip install sisyphusfy` on
|
|
301
|
+
that platform. The installer refuses a deferred target instead of downloading a
|
|
302
|
+
mislabeled binary.
|
|
303
|
+
|
|
304
|
+
## Status
|
|
305
|
+
|
|
306
|
+
This repository contains the product design, roadmap, core implementation, and
|
|
307
|
+
OpenSpec history. The runner, loop engine, agent adapters, workflow
|
|
308
|
+
integrations, and completion hooks are implemented and tested.
|
|
309
|
+
|
|
310
|
+
### Implemented capabilities
|
|
311
|
+
|
|
312
|
+
- Fresh agent sessions with no conversation history carry-over.
|
|
313
|
+
- Durable iteration loop with configurable completion, verification, and timeout.
|
|
314
|
+
- OpenCode adapter (`opencode run --model <model>`), CodeBuddy adapter (`codebuddy -p`), and generic command adapter.
|
|
315
|
+
- Model-chain fallback with retryable/non-retryable failure classification.
|
|
316
|
+
- Workflow adapters: Markdown checklists, JSON predicates, external commands, OpenSpec tasks.
|
|
317
|
+
- Configurable blocked-signal detection across stdout and stderr.
|
|
318
|
+
- Opt-in archive and commit hooks with explicit file allowlists.
|
|
319
|
+
- Default token-efficient prompt when no custom template is configured.
|
|
320
|
+
- Human-friendly CLI: `init`, `run`, `resume`, `status`, `doctor` subcommands.
|
|
321
|
+
- Project configuration in `.sisyphusfy.toml` with documented precedence.
|
|
322
|
+
- Automatic OpenSpec change discovery and task/handoff file detection.
|
|
323
|
+
- Human-readable progress, fallback, blocker, and next-action output.
|
|
324
|
+
|
|
325
|
+
## Name
|
|
326
|
+
|
|
327
|
+
Sisyphus is the figure from Greek mythology condemned to repeatedly push a boulder uphill. Sisyphusfy turns that image into a practical engineering loop: repeated work is allowed, but durable state makes each attempt advance the project.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
Sisyphusfy is released under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sisyphusfy"
|
|
7
|
+
description = "Project-agnostic supervisor for disposable AI-agent sessions"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = []
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = ["pytest>=7.0", "pyyaml>=6.0"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
sisyphusfy = "sisyphusfy.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.dynamic]
|
|
19
|
+
version = { attr = "sisyphusfy.__version__" }
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Sisyphusfy: project-agnostic supervisor for disposable AI-agent sessions."""
|
|
2
|
+
|
|
3
|
+
# Single source of truth for the release version. pyproject.toml reads it
|
|
4
|
+
# dynamically, and tests/test_version_sync.py enforces that every other
|
|
5
|
+
# artifact (npm manifest, npm launcher, installers) agrees with it.
|
|
6
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Agent adapter protocol, registry, and built-in implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FailureClass(str, Enum):
|
|
11
|
+
RETRYABLE_PROVIDER = "retryable_provider"
|
|
12
|
+
RETRYABLE_PROCESS = "retryable_process"
|
|
13
|
+
NON_RETRYABLE = "non_retryable"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class AgentAdapter(Protocol):
|
|
18
|
+
def build_command(
|
|
19
|
+
self,
|
|
20
|
+
working_directory: str,
|
|
21
|
+
prompt: str | None = None,
|
|
22
|
+
) -> list[str]: ...
|
|
23
|
+
|
|
24
|
+
def supports_model(self, model: str) -> bool: ...
|
|
25
|
+
|
|
26
|
+
def classify_failure(self, exit_status: int, stderr: str) -> FailureClass: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class AdapterConfig:
|
|
31
|
+
name: str
|
|
32
|
+
model: str | None = None
|
|
33
|
+
command: list[str] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdapterError(Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ModelChainExhausted(Exception):
|
|
41
|
+
def __init__(self, model_chain: list[str], attempts: list[str]) -> None:
|
|
42
|
+
self.model_chain = model_chain
|
|
43
|
+
self.attempts = attempts
|
|
44
|
+
msg = f"all models exhausted: {', '.join(attempts)}"
|
|
45
|
+
super().__init__(msg)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_RETRYABLE_PROVIDER_MARKERS = (
|
|
49
|
+
"quota",
|
|
50
|
+
"rate_limit",
|
|
51
|
+
"rate limit",
|
|
52
|
+
"429",
|
|
53
|
+
"503",
|
|
54
|
+
"502",
|
|
55
|
+
"provider",
|
|
56
|
+
"overloaded",
|
|
57
|
+
"capacity",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GenericCommandAdapter:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
command: list[str],
|
|
65
|
+
model: str | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
self.command = command
|
|
68
|
+
self.model = model
|
|
69
|
+
|
|
70
|
+
def build_command(
|
|
71
|
+
self,
|
|
72
|
+
working_directory: str,
|
|
73
|
+
prompt: str | None = None,
|
|
74
|
+
) -> list[str]:
|
|
75
|
+
return list(self.command)
|
|
76
|
+
|
|
77
|
+
def supports_model(self, model: str) -> bool:
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
def classify_failure(self, exit_status: int, stderr: str) -> FailureClass:
|
|
81
|
+
lower = stderr.lower()
|
|
82
|
+
for marker in _RETRYABLE_PROVIDER_MARKERS:
|
|
83
|
+
if marker in lower:
|
|
84
|
+
return FailureClass.RETRYABLE_PROVIDER
|
|
85
|
+
if exit_status == -1 or exit_status > 128:
|
|
86
|
+
return FailureClass.RETRYABLE_PROCESS
|
|
87
|
+
return FailureClass.NON_RETRYABLE
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class OpenCodeAdapter:
|
|
91
|
+
def __init__(self, model: str | None = None) -> None:
|
|
92
|
+
self.model = model
|
|
93
|
+
|
|
94
|
+
def build_command(
|
|
95
|
+
self,
|
|
96
|
+
working_directory: str,
|
|
97
|
+
prompt: str | None = None,
|
|
98
|
+
) -> list[str]:
|
|
99
|
+
cmd = ["opencode", "run"]
|
|
100
|
+
if self.model:
|
|
101
|
+
cmd.extend(["--model", self.model])
|
|
102
|
+
if prompt:
|
|
103
|
+
cmd.append(prompt)
|
|
104
|
+
return cmd
|
|
105
|
+
|
|
106
|
+
def supports_model(self, model: str) -> bool:
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def classify_failure(self, exit_status: int, stderr: str) -> FailureClass:
|
|
110
|
+
lower = stderr.lower()
|
|
111
|
+
for marker in _RETRYABLE_PROVIDER_MARKERS:
|
|
112
|
+
if marker in lower:
|
|
113
|
+
return FailureClass.RETRYABLE_PROVIDER
|
|
114
|
+
if exit_status == -1 or exit_status > 128:
|
|
115
|
+
return FailureClass.RETRYABLE_PROCESS
|
|
116
|
+
return FailureClass.NON_RETRYABLE
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CodeBuddyAdapter:
|
|
120
|
+
def __init__(self, model: str | None = None) -> None:
|
|
121
|
+
self.model = model
|
|
122
|
+
|
|
123
|
+
def build_command(
|
|
124
|
+
self,
|
|
125
|
+
working_directory: str,
|
|
126
|
+
prompt: str | None = None,
|
|
127
|
+
) -> list[str]:
|
|
128
|
+
cmd = ["codebuddy", "-p"]
|
|
129
|
+
if self.model:
|
|
130
|
+
cmd.extend(["--model", self.model])
|
|
131
|
+
if prompt:
|
|
132
|
+
cmd.append(prompt)
|
|
133
|
+
return cmd
|
|
134
|
+
|
|
135
|
+
def supports_model(self, model: str) -> bool:
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def classify_failure(self, exit_status: int, stderr: str) -> FailureClass:
|
|
139
|
+
lower = stderr.lower()
|
|
140
|
+
for marker in _RETRYABLE_PROVIDER_MARKERS:
|
|
141
|
+
if marker in lower:
|
|
142
|
+
return FailureClass.RETRYABLE_PROVIDER
|
|
143
|
+
if exit_status == -1 or exit_status > 128:
|
|
144
|
+
return FailureClass.RETRYABLE_PROCESS
|
|
145
|
+
return FailureClass.NON_RETRYABLE
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_BUILTIN_ADAPTERS: dict[str, type] = {
|
|
149
|
+
"opencode": OpenCodeAdapter,
|
|
150
|
+
"codebuddy": CodeBuddyAdapter,
|
|
151
|
+
"generic": GenericCommandAdapter,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class AdapterRegistry:
|
|
156
|
+
def __init__(self) -> None:
|
|
157
|
+
self._adapters: dict[str, type] = dict(_BUILTIN_ADAPTERS)
|
|
158
|
+
|
|
159
|
+
def register(self, name: str, adapter_cls: type) -> None:
|
|
160
|
+
self._adapters[name] = adapter_cls
|
|
161
|
+
|
|
162
|
+
def get(self, name: str) -> type | None:
|
|
163
|
+
return self._adapters.get(name)
|
|
164
|
+
|
|
165
|
+
def list_names(self) -> list[str]:
|
|
166
|
+
return sorted(self._adapters.keys())
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def resolve_adapter(
|
|
170
|
+
config: AdapterConfig,
|
|
171
|
+
agent_command: list[str] | None = None,
|
|
172
|
+
registry: AdapterRegistry | None = None,
|
|
173
|
+
) -> AgentAdapter:
|
|
174
|
+
reg = registry or AdapterRegistry()
|
|
175
|
+
cls = reg.get(config.name)
|
|
176
|
+
if cls is None:
|
|
177
|
+
raise AdapterError(
|
|
178
|
+
f"unknown adapter: {config.name!r} "
|
|
179
|
+
f"(available: {', '.join(reg.list_names())})"
|
|
180
|
+
)
|
|
181
|
+
if config.name == "generic":
|
|
182
|
+
command = config.command if config.command else (agent_command or [])
|
|
183
|
+
return GenericCommandAdapter(command=command, model=config.model)
|
|
184
|
+
return cls(model=config.model)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def try_fallback(
|
|
188
|
+
adapter: AgentAdapter,
|
|
189
|
+
model_chain: list[str],
|
|
190
|
+
working_directory: str,
|
|
191
|
+
prompt: str | None,
|
|
192
|
+
run_fn,
|
|
193
|
+
*,
|
|
194
|
+
max_retries: int = 0,
|
|
195
|
+
) -> tuple[object, list[str]]:
|
|
196
|
+
attempts: list[str] = []
|
|
197
|
+
|
|
198
|
+
models = model_chain if model_chain else [adapter.model or "default"]
|
|
199
|
+
|
|
200
|
+
for model in models:
|
|
201
|
+
if not adapter.supports_model(model):
|
|
202
|
+
continue
|
|
203
|
+
attempts.append(model)
|
|
204
|
+
result = run_fn(model=model)
|
|
205
|
+
|
|
206
|
+
raw_exit = getattr(result, "exit_status", None)
|
|
207
|
+
exit_status = raw_exit if raw_exit is not None else -1
|
|
208
|
+
stderr = getattr(result, "stderr", "") or ""
|
|
209
|
+
classification = adapter.classify_failure(exit_status, stderr)
|
|
210
|
+
|
|
211
|
+
if exit_status == 0:
|
|
212
|
+
return result, attempts
|
|
213
|
+
if classification == FailureClass.NON_RETRYABLE:
|
|
214
|
+
return result, attempts
|
|
215
|
+
if classification == FailureClass.RETRYABLE_PROVIDER:
|
|
216
|
+
continue
|
|
217
|
+
if classification == FailureClass.RETRYABLE_PROCESS:
|
|
218
|
+
continue
|
|
219
|
+
return result, attempts
|
|
220
|
+
|
|
221
|
+
raise ModelChainExhausted(model_chain=model_chain, attempts=attempts)
|