issueloop 0.2.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.
- issueloop-0.2.0/LICENSE +23 -0
- issueloop-0.2.0/PKG-INFO +261 -0
- issueloop-0.2.0/README.md +220 -0
- issueloop-0.2.0/issueloop/__init__.py +71 -0
- issueloop-0.2.0/issueloop/agent_state.py +75 -0
- issueloop-0.2.0/issueloop/check_env.py +78 -0
- issueloop-0.2.0/issueloop/cli.py +79 -0
- issueloop-0.2.0/issueloop/config.py +57 -0
- issueloop-0.2.0/issueloop/db/__init__.py +11 -0
- issueloop-0.2.0/issueloop/db/base.py +22 -0
- issueloop-0.2.0/issueloop/db/local_store.py +102 -0
- issueloop-0.2.0/issueloop/db/supabase_store.py +88 -0
- issueloop-0.2.0/issueloop/folder_reader.py +89 -0
- issueloop-0.2.0/issueloop/live_monitor.py +170 -0
- issueloop-0.2.0/issueloop/llm.py +81 -0
- issueloop-0.2.0/issueloop/log_reader.py +16 -0
- issueloop-0.2.0/issueloop/notify.py +30 -0
- issueloop-0.2.0/issueloop/permissions.py +83 -0
- issueloop-0.2.0/issueloop/server.py +59 -0
- issueloop-0.2.0/issueloop/test_runner.py +106 -0
- issueloop-0.2.0/issueloop/ticket_creator.py +83 -0
- issueloop-0.2.0/issueloop.egg-info/PKG-INFO +261 -0
- issueloop-0.2.0/issueloop.egg-info/SOURCES.txt +32 -0
- issueloop-0.2.0/issueloop.egg-info/dependency_links.txt +1 -0
- issueloop-0.2.0/issueloop.egg-info/entry_points.txt +2 -0
- issueloop-0.2.0/issueloop.egg-info/requires.txt +10 -0
- issueloop-0.2.0/issueloop.egg-info/top_level.txt +1 -0
- issueloop-0.2.0/pyproject.toml +32 -0
- issueloop-0.2.0/setup.cfg +4 -0
- issueloop-0.2.0/tests/test_config.py +29 -0
- issueloop-0.2.0/tests/test_folder_reader.py +19 -0
- issueloop-0.2.0/tests/test_server.py +39 -0
- issueloop-0.2.0/tests/test_test_runner.py +47 -0
- issueloop-0.2.0/tests/test_ticket_store.py +61 -0
issueloop-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE (MIT)
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 amareshhebbar
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
issueloop-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: issueloop
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Finds failing tests, splits failures into independent tickets, stores them, hands them out one at a time.
|
|
5
|
+
Author: onenot8
|
|
6
|
+
License: LICENSE (MIT)
|
|
7
|
+
|
|
8
|
+
MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 amareshhebbar
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Requires-Dist: requests
|
|
33
|
+
Requires-Dist: pyyaml
|
|
34
|
+
Requires-Dist: pathspec
|
|
35
|
+
Provides-Extra: supabase
|
|
36
|
+
Requires-Dist: supabase; extra == "supabase"
|
|
37
|
+
Requires-Dist: python-dotenv; extra == "supabase"
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# IssueLoop
|
|
43
|
+
|
|
44
|
+
Finds failing tests (batch or live). Splits failures into independent
|
|
45
|
+
tickets via an LLM, with automatic priority fallback across providers.
|
|
46
|
+
Stores them. Hands them out one at a time. Optionally proposes and
|
|
47
|
+
applies fixes under an explicit, auditable permission allowlist.
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+

|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Why this exists
|
|
56
|
+
|
|
57
|
+
Bugs sit unfixed because someone has to notice them first. IssueLoop
|
|
58
|
+
reads the test suite directly — a failing test is the bug report. It
|
|
59
|
+
can also watch a live-running process or log file, for bugs that only
|
|
60
|
+
show up outside a test run. No human has to spot it, and no human has
|
|
61
|
+
to manually triage which failures are actually distinct problems.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
Not yet on PyPI — install from git:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python3 -m venv .venv
|
|
69
|
+
source .venv/bin/activate
|
|
70
|
+
pip install "git+https://github.com/onenot8/issueLoop@phase-2"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or for local development:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/onenot8/issueLoop
|
|
77
|
+
cd issueLoop
|
|
78
|
+
pip install -e ".[dev]"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Local SQLite backend, zero setup. No database account, no API key required to try it.
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import issueloop
|
|
87
|
+
|
|
88
|
+
issueloop.use(
|
|
89
|
+
database="local", # or "supabase" for a hosted backup
|
|
90
|
+
llm={"providers": [ # priority list — falls back automatically
|
|
91
|
+
{"provider": "anthropic", "model": "claude-sonnet-4-6", "api_key": "sk-..."},
|
|
92
|
+
{"provider": "ollama", "model": "qwen2.5-coder:7b"}, # free, local, default if omitted
|
|
93
|
+
]},
|
|
94
|
+
notify={"webhook": "https://your-endpoint"}, # fires if IssueLoop itself breaks
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
issueloop.scan_repo("repos/myrepo")
|
|
98
|
+
issueloop.run_tests("myrepo")
|
|
99
|
+
issueloop.create_tickets("myrepo")
|
|
100
|
+
|
|
101
|
+
ticket = issueloop.get_top_error("myrepo") # claims one, marks in_progress
|
|
102
|
+
issueloop.resolve(ticket["id"]) # your side fixed it
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Live monitoring
|
|
106
|
+
|
|
107
|
+
Watch a process IssueLoop spawns itself, or tail a log file an
|
|
108
|
+
already-running process writes to:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from issueloop import watch_process, watch_log_file, stop_watch
|
|
112
|
+
|
|
113
|
+
handle = watch_process("myrepo", "python3 main.py", cwd="/path/to/repo", debounce_seconds=3.0)
|
|
114
|
+
# ...
|
|
115
|
+
stop_watch(handle)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or from the CLI, in the foreground:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
issueloop watch myrepo --command "python3 main.py" --cwd /path/to/repo --debounce 3.0
|
|
122
|
+
issueloop watch myrepo --log-file /path/to/app.log --debounce 3.0
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Errors are debounced (default 3s of quiet) so a single multi-line
|
|
126
|
+
traceback becomes one ticket, not dozens. Detected errors land in the
|
|
127
|
+
same log format the batch test runner uses — `issueloop tickets` picks
|
|
128
|
+
them up with no extra steps.
|
|
129
|
+
|
|
130
|
+
## Fix-apply layer
|
|
131
|
+
|
|
132
|
+
Off by default — every command needs an explicit allowlist entry:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
# config/permission.yaml
|
|
136
|
+
per_repo:
|
|
137
|
+
myrepo:
|
|
138
|
+
allowed_patterns:
|
|
139
|
+
- 'sed -i .* somefile\.py'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
issueloop.propose_fix(ticket_id, "sed -i 's/old/new/' somefile.py")
|
|
144
|
+
result = issueloop.apply_fix(ticket_id)
|
|
145
|
+
# {"status": "resolved" | "retry" | "escalated" | "denied", ...}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`apply_fix` re-runs the ticket's associated test after applying the
|
|
149
|
+
fix and only resolves it if the test actually passes. Failures retry
|
|
150
|
+
up to a configurable limit, then escalate to `needs_human`. Every
|
|
151
|
+
permission decision is audited — `issueloop.get_permission_audit_log()`.
|
|
152
|
+
|
|
153
|
+
## Use it from the CLI
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
issueloop check-env
|
|
157
|
+
issueloop scan repos/myrepo
|
|
158
|
+
issueloop test myrepo
|
|
159
|
+
issueloop tickets myrepo
|
|
160
|
+
issueloop next myrepo
|
|
161
|
+
issueloop watch myrepo --command "..." | --log-file ...
|
|
162
|
+
issueloop cleanup --days 30
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Use it from Node, Go, or anything else
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
issueloop serve --port 8787
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const r = await fetch("http://localhost:8787/errors/top?repo=myrepo");
|
|
173
|
+
const ticket = await r.json();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`serve` is localhost-only, no auth — put it behind your own gateway if
|
|
177
|
+
you ever expose it further.
|
|
178
|
+
|
|
179
|
+
## Full API
|
|
180
|
+
|
|
181
|
+
Bug query — `get_all_bugs`, `get_unresolved_bugs`, `get_resolved_bugs`,
|
|
182
|
+
`get_failed_bugs`, `get_bugs_needing_human`, `get_bugs_by_status`,
|
|
183
|
+
`get_bugs_by_priority`, `get_bug`, `get_bug_count`,
|
|
184
|
+
`get_bug_count_by_status`, `search_bugs`, `get_oldest_bug`, `get_newest_bug`
|
|
185
|
+
|
|
186
|
+
Lifecycle — `get_top_error`, `get_all_errors`, `resolve`, `fail`,
|
|
187
|
+
`escalate`, `reassign`, `retry_bug`, `get_bug_attempts`, `bulk_resolve`
|
|
188
|
+
|
|
189
|
+
Fix-apply — `propose_fix`, `apply_fix`, `check_permission`,
|
|
190
|
+
`get_permission_audit_log`
|
|
191
|
+
|
|
192
|
+
Scan / test — `scan_repo`, `get_file_inventory`, `run_tests`,
|
|
193
|
+
`run_single_test`, `create_tickets`
|
|
194
|
+
|
|
195
|
+
Live monitoring — `watch_process`, `watch_log_file`, `stop_watch`,
|
|
196
|
+
`list_active_watchers`
|
|
197
|
+
|
|
198
|
+
LLM / tokens — `get_token_consumption`,
|
|
199
|
+
`get_token_consumption_by_provider`, `get_llm_call_history`,
|
|
200
|
+
`get_llm_provider_status`
|
|
201
|
+
|
|
202
|
+
Database — `cleanup`, `purge_repo`, `get_database_stats`,
|
|
203
|
+
`export_bugs`, `reap_stale_bugs`, `rotate_logs`
|
|
204
|
+
|
|
205
|
+
Notifications — `get_crash_log`, `get_notification_config`
|
|
206
|
+
|
|
207
|
+
Config / utility — `use`, `get_config`, `list_repos`, `health_check`
|
|
208
|
+
|
|
209
|
+
## Config — every option
|
|
210
|
+
|
|
211
|
+
| Field | Values | Default |
|
|
212
|
+
| ---------------------- | -------------------------------------- | --------------------- |
|
|
213
|
+
| `database` | `"local"`, `"supabase"` | `"local"` |
|
|
214
|
+
| `database_path` | any path | `data/issueloop.db` |
|
|
215
|
+
| `retention_days` | int | `30` |
|
|
216
|
+
| `llm.providers` | list of provider dicts, priority order | single ollama default |
|
|
217
|
+
| `llm.provider` | `"ollama"`, `"anthropic"`, `"openai"` | `"ollama"` |
|
|
218
|
+
| `llm.apiKey` | string | none (required for anthropic/openai) |
|
|
219
|
+
| `llm.tokenSize` | int | `1024` |
|
|
220
|
+
| `notify.webhook` | URL | none |
|
|
221
|
+
|
|
222
|
+
Both `apiKey`/`api_key` and `tokenSize`/`token_size` work — camelCase
|
|
223
|
+
or snake_case, your call. A single `llm={...}` dict still works exactly
|
|
224
|
+
as before; wrap multiple in `llm={"providers": [...]}` for automatic
|
|
225
|
+
fallback.
|
|
226
|
+
|
|
227
|
+
Config files (`config/permission.yaml`, `config/provider_config.yaml`)
|
|
228
|
+
resolve in this order: an explicit env var
|
|
229
|
+
(`ISSUELOOP_PERMISSION_PATH` / `ISSUELOOP_PROVIDER_CONFIG_PATH`) →
|
|
230
|
+
`./config/` relative to your current directory → `config/` in the
|
|
231
|
+
IssueLoop checkout → bundled package defaults. This means a real
|
|
232
|
+
`pip install` (not just `-e .`) still works correctly even without a
|
|
233
|
+
project-local `config/` directory.
|
|
234
|
+
|
|
235
|
+
## Why trust this
|
|
236
|
+
|
|
237
|
+
- Every module has a test, and the tests run against real behavior —
|
|
238
|
+
a real SQLite file, a real HTTP server on a real socket — not just
|
|
239
|
+
mocks pretending things work.
|
|
240
|
+
- Real bugs were caught and fixed by actually running this code, not
|
|
241
|
+
by reading it — including a prompt that only ever sent `stderr`
|
|
242
|
+
to the LLM triage step while pytest (and most test runners) report
|
|
243
|
+
failures on `stdout`, and a config-bundling gap that made the
|
|
244
|
+
permission system silently deny everything under a real (non-editable)
|
|
245
|
+
install. Both have regression coverage now.
|
|
246
|
+
- Local-first by default. Nothing phones home, nothing requires an
|
|
247
|
+
account to try.
|
|
248
|
+
- Read the code — it's small enough to actually read. That's on
|
|
249
|
+
purpose. File-by-file build order: `BUILD_ORDER.md`.
|
|
250
|
+
|
|
251
|
+
## Testing
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pytest tests/ -v
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Full walkthrough, including live monitoring and the fix-apply layer: `TESTING.md`.
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# IssueLoop
|
|
2
|
+
|
|
3
|
+
Finds failing tests (batch or live). Splits failures into independent
|
|
4
|
+
tickets via an LLM, with automatic priority fallback across providers.
|
|
5
|
+
Stores them. Hands them out one at a time. Optionally proposes and
|
|
6
|
+
applies fixes under an explicit, auditable permission allowlist.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why this exists
|
|
15
|
+
|
|
16
|
+
Bugs sit unfixed because someone has to notice them first. IssueLoop
|
|
17
|
+
reads the test suite directly — a failing test is the bug report. It
|
|
18
|
+
can also watch a live-running process or log file, for bugs that only
|
|
19
|
+
show up outside a test run. No human has to spot it, and no human has
|
|
20
|
+
to manually triage which failures are actually distinct problems.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Not yet on PyPI — install from git:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 -m venv .venv
|
|
28
|
+
source .venv/bin/activate
|
|
29
|
+
pip install "git+https://github.com/onenot8/issueLoop@phase-2"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or for local development:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/onenot8/issueLoop
|
|
36
|
+
cd issueLoop
|
|
37
|
+
pip install -e ".[dev]"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Local SQLite backend, zero setup. No database account, no API key required to try it.
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import issueloop
|
|
46
|
+
|
|
47
|
+
issueloop.use(
|
|
48
|
+
database="local", # or "supabase" for a hosted backup
|
|
49
|
+
llm={"providers": [ # priority list — falls back automatically
|
|
50
|
+
{"provider": "anthropic", "model": "claude-sonnet-4-6", "api_key": "sk-..."},
|
|
51
|
+
{"provider": "ollama", "model": "qwen2.5-coder:7b"}, # free, local, default if omitted
|
|
52
|
+
]},
|
|
53
|
+
notify={"webhook": "https://your-endpoint"}, # fires if IssueLoop itself breaks
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
issueloop.scan_repo("repos/myrepo")
|
|
57
|
+
issueloop.run_tests("myrepo")
|
|
58
|
+
issueloop.create_tickets("myrepo")
|
|
59
|
+
|
|
60
|
+
ticket = issueloop.get_top_error("myrepo") # claims one, marks in_progress
|
|
61
|
+
issueloop.resolve(ticket["id"]) # your side fixed it
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Live monitoring
|
|
65
|
+
|
|
66
|
+
Watch a process IssueLoop spawns itself, or tail a log file an
|
|
67
|
+
already-running process writes to:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from issueloop import watch_process, watch_log_file, stop_watch
|
|
71
|
+
|
|
72
|
+
handle = watch_process("myrepo", "python3 main.py", cwd="/path/to/repo", debounce_seconds=3.0)
|
|
73
|
+
# ...
|
|
74
|
+
stop_watch(handle)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or from the CLI, in the foreground:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
issueloop watch myrepo --command "python3 main.py" --cwd /path/to/repo --debounce 3.0
|
|
81
|
+
issueloop watch myrepo --log-file /path/to/app.log --debounce 3.0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Errors are debounced (default 3s of quiet) so a single multi-line
|
|
85
|
+
traceback becomes one ticket, not dozens. Detected errors land in the
|
|
86
|
+
same log format the batch test runner uses — `issueloop tickets` picks
|
|
87
|
+
them up with no extra steps.
|
|
88
|
+
|
|
89
|
+
## Fix-apply layer
|
|
90
|
+
|
|
91
|
+
Off by default — every command needs an explicit allowlist entry:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
# config/permission.yaml
|
|
95
|
+
per_repo:
|
|
96
|
+
myrepo:
|
|
97
|
+
allowed_patterns:
|
|
98
|
+
- 'sed -i .* somefile\.py'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
issueloop.propose_fix(ticket_id, "sed -i 's/old/new/' somefile.py")
|
|
103
|
+
result = issueloop.apply_fix(ticket_id)
|
|
104
|
+
# {"status": "resolved" | "retry" | "escalated" | "denied", ...}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`apply_fix` re-runs the ticket's associated test after applying the
|
|
108
|
+
fix and only resolves it if the test actually passes. Failures retry
|
|
109
|
+
up to a configurable limit, then escalate to `needs_human`. Every
|
|
110
|
+
permission decision is audited — `issueloop.get_permission_audit_log()`.
|
|
111
|
+
|
|
112
|
+
## Use it from the CLI
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
issueloop check-env
|
|
116
|
+
issueloop scan repos/myrepo
|
|
117
|
+
issueloop test myrepo
|
|
118
|
+
issueloop tickets myrepo
|
|
119
|
+
issueloop next myrepo
|
|
120
|
+
issueloop watch myrepo --command "..." | --log-file ...
|
|
121
|
+
issueloop cleanup --days 30
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Use it from Node, Go, or anything else
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
issueloop serve --port 8787
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
const r = await fetch("http://localhost:8787/errors/top?repo=myrepo");
|
|
132
|
+
const ticket = await r.json();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`serve` is localhost-only, no auth — put it behind your own gateway if
|
|
136
|
+
you ever expose it further.
|
|
137
|
+
|
|
138
|
+
## Full API
|
|
139
|
+
|
|
140
|
+
Bug query — `get_all_bugs`, `get_unresolved_bugs`, `get_resolved_bugs`,
|
|
141
|
+
`get_failed_bugs`, `get_bugs_needing_human`, `get_bugs_by_status`,
|
|
142
|
+
`get_bugs_by_priority`, `get_bug`, `get_bug_count`,
|
|
143
|
+
`get_bug_count_by_status`, `search_bugs`, `get_oldest_bug`, `get_newest_bug`
|
|
144
|
+
|
|
145
|
+
Lifecycle — `get_top_error`, `get_all_errors`, `resolve`, `fail`,
|
|
146
|
+
`escalate`, `reassign`, `retry_bug`, `get_bug_attempts`, `bulk_resolve`
|
|
147
|
+
|
|
148
|
+
Fix-apply — `propose_fix`, `apply_fix`, `check_permission`,
|
|
149
|
+
`get_permission_audit_log`
|
|
150
|
+
|
|
151
|
+
Scan / test — `scan_repo`, `get_file_inventory`, `run_tests`,
|
|
152
|
+
`run_single_test`, `create_tickets`
|
|
153
|
+
|
|
154
|
+
Live monitoring — `watch_process`, `watch_log_file`, `stop_watch`,
|
|
155
|
+
`list_active_watchers`
|
|
156
|
+
|
|
157
|
+
LLM / tokens — `get_token_consumption`,
|
|
158
|
+
`get_token_consumption_by_provider`, `get_llm_call_history`,
|
|
159
|
+
`get_llm_provider_status`
|
|
160
|
+
|
|
161
|
+
Database — `cleanup`, `purge_repo`, `get_database_stats`,
|
|
162
|
+
`export_bugs`, `reap_stale_bugs`, `rotate_logs`
|
|
163
|
+
|
|
164
|
+
Notifications — `get_crash_log`, `get_notification_config`
|
|
165
|
+
|
|
166
|
+
Config / utility — `use`, `get_config`, `list_repos`, `health_check`
|
|
167
|
+
|
|
168
|
+
## Config — every option
|
|
169
|
+
|
|
170
|
+
| Field | Values | Default |
|
|
171
|
+
| ---------------------- | -------------------------------------- | --------------------- |
|
|
172
|
+
| `database` | `"local"`, `"supabase"` | `"local"` |
|
|
173
|
+
| `database_path` | any path | `data/issueloop.db` |
|
|
174
|
+
| `retention_days` | int | `30` |
|
|
175
|
+
| `llm.providers` | list of provider dicts, priority order | single ollama default |
|
|
176
|
+
| `llm.provider` | `"ollama"`, `"anthropic"`, `"openai"` | `"ollama"` |
|
|
177
|
+
| `llm.apiKey` | string | none (required for anthropic/openai) |
|
|
178
|
+
| `llm.tokenSize` | int | `1024` |
|
|
179
|
+
| `notify.webhook` | URL | none |
|
|
180
|
+
|
|
181
|
+
Both `apiKey`/`api_key` and `tokenSize`/`token_size` work — camelCase
|
|
182
|
+
or snake_case, your call. A single `llm={...}` dict still works exactly
|
|
183
|
+
as before; wrap multiple in `llm={"providers": [...]}` for automatic
|
|
184
|
+
fallback.
|
|
185
|
+
|
|
186
|
+
Config files (`config/permission.yaml`, `config/provider_config.yaml`)
|
|
187
|
+
resolve in this order: an explicit env var
|
|
188
|
+
(`ISSUELOOP_PERMISSION_PATH` / `ISSUELOOP_PROVIDER_CONFIG_PATH`) →
|
|
189
|
+
`./config/` relative to your current directory → `config/` in the
|
|
190
|
+
IssueLoop checkout → bundled package defaults. This means a real
|
|
191
|
+
`pip install` (not just `-e .`) still works correctly even without a
|
|
192
|
+
project-local `config/` directory.
|
|
193
|
+
|
|
194
|
+
## Why trust this
|
|
195
|
+
|
|
196
|
+
- Every module has a test, and the tests run against real behavior —
|
|
197
|
+
a real SQLite file, a real HTTP server on a real socket — not just
|
|
198
|
+
mocks pretending things work.
|
|
199
|
+
- Real bugs were caught and fixed by actually running this code, not
|
|
200
|
+
by reading it — including a prompt that only ever sent `stderr`
|
|
201
|
+
to the LLM triage step while pytest (and most test runners) report
|
|
202
|
+
failures on `stdout`, and a config-bundling gap that made the
|
|
203
|
+
permission system silently deny everything under a real (non-editable)
|
|
204
|
+
install. Both have regression coverage now.
|
|
205
|
+
- Local-first by default. Nothing phones home, nothing requires an
|
|
206
|
+
account to try.
|
|
207
|
+
- Read the code — it's small enough to actually read. That's on
|
|
208
|
+
purpose. File-by-file build order: `BUILD_ORDER.md`.
|
|
209
|
+
|
|
210
|
+
## Testing
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
pytest tests/ -v
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Full walkthrough, including live monitoring and the fix-apply layer: `TESTING.md`.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .config import use, get_config
|
|
6
|
+
from .db import get_backend
|
|
7
|
+
from .agent_state import TaskStatus
|
|
8
|
+
from . import folder_reader, test_runner,ticket_creator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _backend():
|
|
12
|
+
cfg = get_config()
|
|
13
|
+
return get_backend(cfg.database, path=cfg.database_path)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def scan_repo(repo_path: str):
|
|
17
|
+
out_file = folder_reader.write_inventory(Path(repo_path).resolve())
|
|
18
|
+
import json
|
|
19
|
+
return json.loads(out_file.read_text())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_tests(repo_name: str):
|
|
23
|
+
return test_runner.run_tests(repo_name)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_tickets(repo_name: str):
|
|
27
|
+
tickets = ticket_creator.create_tickets_for_repo(repo_name)
|
|
28
|
+
return [_ticket_to_dict(t) for t in tickets]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_top_error(repo_name: str):
|
|
32
|
+
ticket = _backend().dispense_next(repo_name)
|
|
33
|
+
return _ticket_to_dict(ticket) if ticket else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_all_errors(repo_name: Optional[str] = None):
|
|
37
|
+
return [_ticket_to_dict(t) for t in _backend().get_open_tickets(repo_name)]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve(ticket_id: str):
|
|
41
|
+
_backend().update_ticket(ticket_id, status=TaskStatus.DONE)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fail(ticket_id: str):
|
|
45
|
+
_backend().update_ticket(ticket_id, status=TaskStatus.FAILED)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cleanup(older_than_days: Optional[int] = None, repo: Optional[str] = None):
|
|
49
|
+
days = older_than_days if older_than_days is not None else get_config().retention_days
|
|
50
|
+
return _backend().purge_old(days, repo)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _ticket_to_dict(ticket):
|
|
54
|
+
return {
|
|
55
|
+
"id": ticket.id,
|
|
56
|
+
"repo": ticket.repo,
|
|
57
|
+
"priority": ticket.priority.value,
|
|
58
|
+
"status": ticket.status.value,
|
|
59
|
+
"error_summary": ticket.error_summary,
|
|
60
|
+
"raw_log_ref": ticket.raw_log_ref,
|
|
61
|
+
"command": ticket.command,
|
|
62
|
+
"test_id": ticket.test_id,
|
|
63
|
+
"created_at": str(ticket.created_at),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"use", "get_config",
|
|
69
|
+
"scan_repo", "run_tests", "create_tickets",
|
|
70
|
+
"get_top_error", "get_all_errors", "resolve", "fail", "cleanup",
|
|
71
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TaskStatus(str, Enum):
|
|
8
|
+
PENDING = "pending"
|
|
9
|
+
IN_PROGRESS = "in_progress"
|
|
10
|
+
BLOCKED = "blocked"
|
|
11
|
+
DONE = "done"
|
|
12
|
+
FAILED = "failed"
|
|
13
|
+
NEEDS_HUMAN = "needs_human"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TicketPriority(str, Enum):
|
|
17
|
+
BLOCKING = "blocking"
|
|
18
|
+
HIGH = "high"
|
|
19
|
+
NORMAL = "normal"
|
|
20
|
+
LOW = "low"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SubTask:
|
|
25
|
+
id: str
|
|
26
|
+
description: str
|
|
27
|
+
depends_on: list[str] = field(default_factory=list)
|
|
28
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
29
|
+
result: Optional[dict[str, Any]] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Ticket:
|
|
34
|
+
id: str
|
|
35
|
+
repo: str
|
|
36
|
+
error_summary: str
|
|
37
|
+
raw_log_ref: str
|
|
38
|
+
priority: TicketPriority
|
|
39
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
40
|
+
attempts: int = 0
|
|
41
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
42
|
+
resolved_at: Optional[datetime] = None
|
|
43
|
+
escalation_summary: Optional[str] = None
|
|
44
|
+
command: Optional[str] = None
|
|
45
|
+
test_id: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class AgentState:
|
|
50
|
+
allowed_commands: list[str] = field(default_factory=list)
|
|
51
|
+
create_pr_permission: bool = False
|
|
52
|
+
supabase_write_permission: bool = False
|
|
53
|
+
repo: str = ""
|
|
54
|
+
branch: str = ""
|
|
55
|
+
current_task_id: str = ""
|
|
56
|
+
current_task: Optional[SubTask] = None
|
|
57
|
+
previous_task_and_result: Optional[dict[str, Any]] = None
|
|
58
|
+
next_task_id: Optional[str] = None
|
|
59
|
+
queue_independent_tasks: list[SubTask] = field(default_factory=list)
|
|
60
|
+
queue_dependent_task: list[SubTask] = field(default_factory=list)
|
|
61
|
+
active_tickets: list[Ticket] = field(default_factory=list)
|
|
62
|
+
message_from_user: Optional[str] = None
|
|
63
|
+
pending_confirmation: bool = False
|
|
64
|
+
goal: str = ""
|
|
65
|
+
what_to_do: list[str] = field(default_factory=list)
|
|
66
|
+
what_not_to_do: list[str] = field(default_factory=list)
|
|
67
|
+
overall_points: int = 0
|
|
68
|
+
current_task_points: int = 0
|
|
69
|
+
max_retries_per_ticket: int = 3
|
|
70
|
+
log_cache_ref: str = ""
|
|
71
|
+
last_error: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def new_run_state(repo: str, goal: str, allowed_commands: list[str]):
|
|
75
|
+
return AgentState(repo=repo, goal=goal, allowed_commands=allowed_commands)
|