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.
Files changed (34) hide show
  1. issueloop-0.2.0/LICENSE +23 -0
  2. issueloop-0.2.0/PKG-INFO +261 -0
  3. issueloop-0.2.0/README.md +220 -0
  4. issueloop-0.2.0/issueloop/__init__.py +71 -0
  5. issueloop-0.2.0/issueloop/agent_state.py +75 -0
  6. issueloop-0.2.0/issueloop/check_env.py +78 -0
  7. issueloop-0.2.0/issueloop/cli.py +79 -0
  8. issueloop-0.2.0/issueloop/config.py +57 -0
  9. issueloop-0.2.0/issueloop/db/__init__.py +11 -0
  10. issueloop-0.2.0/issueloop/db/base.py +22 -0
  11. issueloop-0.2.0/issueloop/db/local_store.py +102 -0
  12. issueloop-0.2.0/issueloop/db/supabase_store.py +88 -0
  13. issueloop-0.2.0/issueloop/folder_reader.py +89 -0
  14. issueloop-0.2.0/issueloop/live_monitor.py +170 -0
  15. issueloop-0.2.0/issueloop/llm.py +81 -0
  16. issueloop-0.2.0/issueloop/log_reader.py +16 -0
  17. issueloop-0.2.0/issueloop/notify.py +30 -0
  18. issueloop-0.2.0/issueloop/permissions.py +83 -0
  19. issueloop-0.2.0/issueloop/server.py +59 -0
  20. issueloop-0.2.0/issueloop/test_runner.py +106 -0
  21. issueloop-0.2.0/issueloop/ticket_creator.py +83 -0
  22. issueloop-0.2.0/issueloop.egg-info/PKG-INFO +261 -0
  23. issueloop-0.2.0/issueloop.egg-info/SOURCES.txt +32 -0
  24. issueloop-0.2.0/issueloop.egg-info/dependency_links.txt +1 -0
  25. issueloop-0.2.0/issueloop.egg-info/entry_points.txt +2 -0
  26. issueloop-0.2.0/issueloop.egg-info/requires.txt +10 -0
  27. issueloop-0.2.0/issueloop.egg-info/top_level.txt +1 -0
  28. issueloop-0.2.0/pyproject.toml +32 -0
  29. issueloop-0.2.0/setup.cfg +4 -0
  30. issueloop-0.2.0/tests/test_config.py +29 -0
  31. issueloop-0.2.0/tests/test_folder_reader.py +19 -0
  32. issueloop-0.2.0/tests/test_server.py +39 -0
  33. issueloop-0.2.0/tests/test_test_runner.py +47 -0
  34. issueloop-0.2.0/tests/test_ticket_store.py +61 -0
@@ -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.
@@ -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
+ ![license](https://img.shields.io/badge/license-MIT-blue)
50
+ ![python](https://img.shields.io/badge/python-3.10%2B-blue)
51
+ ![status](https://img.shields.io/badge/status-phase--2-orange)
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
+ ![license](https://img.shields.io/badge/license-MIT-blue)
9
+ ![python](https://img.shields.io/badge/python-3.10%2B-blue)
10
+ ![status](https://img.shields.io/badge/status-phase--2-orange)
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)