athanore 0.0.1__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 (52) hide show
  1. athanore-0.0.1/.gitignore +29 -0
  2. athanore-0.0.1/.python-version +1 -0
  3. athanore-0.0.1/CLAUDE.md +75 -0
  4. athanore-0.0.1/IDEAS.md +17 -0
  5. athanore-0.0.1/LICENSE +21 -0
  6. athanore-0.0.1/PKG-INFO +437 -0
  7. athanore-0.0.1/README.md +419 -0
  8. athanore-0.0.1/agent_run.py +129 -0
  9. athanore-0.0.1/athanore/__init__.py +23 -0
  10. athanore-0.0.1/athanore/agent_adapters/__init__.py +1 -0
  11. athanore-0.0.1/athanore/agent_adapters/base.py +55 -0
  12. athanore-0.0.1/athanore/agent_adapters/subprocess_adapter.py +238 -0
  13. athanore-0.0.1/athanore/config.py +48 -0
  14. athanore-0.0.1/athanore/dashboard.py +2382 -0
  15. athanore-0.0.1/athanore/dispatcher.py +212 -0
  16. athanore-0.0.1/athanore/event_bus.py +171 -0
  17. athanore-0.0.1/athanore/http_api.py +539 -0
  18. athanore-0.0.1/athanore/queue_adapters/__init__.py +1 -0
  19. athanore-0.0.1/athanore/queue_adapters/base.py +74 -0
  20. athanore-0.0.1/athanore/queue_adapters/json_file.py +189 -0
  21. athanore-0.0.1/athanore/queue_operations.py +34 -0
  22. athanore-0.0.1/athanore/route_operations.py +68 -0
  23. athanore-0.0.1/athanore/router.py +576 -0
  24. athanore-0.0.1/athanore/task_operations.py +77 -0
  25. athanore-0.0.1/athanore.sh +89 -0
  26. athanore-0.0.1/claude_adapter.py +344 -0
  27. athanore-0.0.1/hello-world.md +3 -0
  28. athanore-0.0.1/openapi.yaml +858 -0
  29. athanore-0.0.1/plan_payload.json +3 -0
  30. athanore-0.0.1/plan_update_payload.json +3 -0
  31. athanore-0.0.1/planka_backend.py +359 -0
  32. athanore-0.0.1/pyproject.toml +59 -0
  33. athanore-0.0.1/task_update.json +3 -0
  34. athanore-0.0.1/tests/__init__.py +0 -0
  35. athanore-0.0.1/tests/conftest.py +110 -0
  36. athanore-0.0.1/tests/test_agents.py +76 -0
  37. athanore-0.0.1/tests/test_claude_adapter.py +508 -0
  38. athanore-0.0.1/tests/test_config.py +163 -0
  39. athanore-0.0.1/tests/test_dashboard.py +2655 -0
  40. athanore-0.0.1/tests/test_dispatcher.py +456 -0
  41. athanore-0.0.1/tests/test_event_bus.py +337 -0
  42. athanore-0.0.1/tests/test_http_api.py +847 -0
  43. athanore-0.0.1/tests/test_json_adapter.py +291 -0
  44. athanore-0.0.1/tests/test_queue_http_api.py +306 -0
  45. athanore-0.0.1/tests/test_queue_operations.py +107 -0
  46. athanore-0.0.1/tests/test_route_operations.py +118 -0
  47. athanore-0.0.1/tests/test_router.py +2722 -0
  48. athanore-0.0.1/tests/test_sse_endpoint.py +184 -0
  49. athanore-0.0.1/tests/test_subprocess_adapter.py +705 -0
  50. athanore-0.0.1/tests/test_task_operations.py +79 -0
  51. athanore-0.0.1/uv.lock +735 -0
  52. athanore-0.0.1/v2.py +102 -0
@@ -0,0 +1,29 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ .env
13
+ .claude/
14
+ .pytest_cache/
15
+ .playwright-mcp/
16
+
17
+ # Coverage
18
+ htmlcov/
19
+ .coverage
20
+ coverage.xml
21
+
22
+ # Temp files
23
+ tmp_*.json
24
+ .tmp_*.json
25
+ .artificer.pid
26
+ artificer.log
27
+
28
+
29
+ *.png
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,75 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project overview
4
+
5
+ athanore — programmatic-only library that polls task queues, dispatches agent subprocesses, and exposes an HTTP API for agents. Python 3.13+, uses `uv` for dependency management.
6
+
7
+ ## Commands
8
+
9
+ - Tests: `.venv/bin/pytest`
10
+ - Tests with HTML report: `.venv/bin/pytest --cov-report=html`
11
+ - Tests with XML report: `.venv/bin/pytest --cov-report=xml`
12
+ - Install dev deps: `uv pip install -e ".[dev]"`
13
+
14
+ ## Project structure
15
+
16
+ - Public API: `athanore/__init__.py`
17
+ - Entry point: `AgentDispatcher` class with `@route()` decorator API in `athanore/dispatcher.py`
18
+ - Config dataclasses: `athanore/config.py`
19
+ - Queue adapters: `athanore/queue_adapters/` (`QueueAdapter` protocol in `base.py`, `JsonFileAdapter` in `json_file.py`)
20
+ - Agent adapters: `athanore/agent_adapters/` (`AgentAdapter` protocol in `base.py`, `SubprocessAgentAdapter` in `subprocess_adapter.py`)
21
+ - User-land adapters (repo root, not part of the library):
22
+ - `claude_adapter.py` — `ClaudeAgentAdapter` (Claude agent adapter using `claude-code-sdk`)
23
+ - `planka_backend.py` — `PlankaBackend` (Planka queue backend)
24
+ - Entry point: `v2.py`
25
+
26
+ ## Dashboard / Frontend
27
+
28
+ - Dashboard served at `/dashboard` (home) and `/dashboard/routes` (route config)
29
+ - All HTML is rendered inline in `athanore/dashboard.py` (no template files — HTML is built as Python strings)
30
+ - Dashboard uses SSE for live updates
31
+ - Figma designs: https://www.figma.com/design/Ntt3DbNFoD8HbYQ93W8SOo/Artificer
32
+ - Route Configuration page: node-id `2-168`
33
+
34
+ ## Conventions
35
+
36
+ - Keep `README.md` in sync when adding/changing constructor args, endpoints, or backend adapters
37
+ - Queue adapters implement the `QueueAdapter` protocol in `athanore/queue_adapters/base.py`
38
+ - Agent adapters implement the `AgentAdapter` protocol in `athanore/agent_adapters/base.py`
39
+
40
+ ## Subagent instructions
41
+
42
+ ### software-engineer
43
+
44
+ When working on dashboard or frontend changes:
45
+
46
+ 1. **Reference Figma designs** — Use the Figma MCP tools to view the target design before implementing UI changes. The Figma file key is `Ntt3DbNFoD8HbYQ93W8SOo`. Use `mcp__figma__get_screenshot` or `mcp__figma__get_design_context` with the relevant node ID to see the design spec.
47
+ 2. **Test with Playwright** — After making frontend changes, verify them using the Playwright MCP tools:
48
+ - `mcp__playwright__browser_navigate` to `http://localhost:8000/dashboard` (or the relevant page)
49
+ - `mcp__playwright__browser_snapshot` to inspect the page structure
50
+ - `mcp__playwright__browser_take_screenshot` to visually verify the result
51
+ - `mcp__playwright__browser_click`, `mcp__playwright__browser_type`, etc. to test interactions
52
+ - `mcp__playwright__browser_console_messages` to check for JS errors
53
+ 3. Compare the Playwright screenshot against the Figma design to confirm the implementation matches.
54
+
55
+ ### product-manager
56
+
57
+ When creating stories for dashboard or frontend work:
58
+
59
+ 1. **Review Figma designs** — Use `mcp__figma__get_screenshot` or `mcp__figma__get_design_context` with file key `Ntt3DbNFoD8HbYQ93W8SOo` and the relevant node ID to understand the design intent, layout, and components.
60
+ 2. **Reference specific designs in stories** — Include the Figma node IDs in story descriptions so engineers and QA know exactly which design to implement and test against.
61
+ 3. **Check current state** — Use `mcp__figma__get_metadata` to explore the Figma file structure and discover available pages/frames when scoping work.
62
+
63
+ ### qa-engineer
64
+
65
+ When testing dashboard or frontend features:
66
+
67
+ 1. **Load Figma designs as the source of truth** — Use `mcp__figma__get_screenshot` with file key `Ntt3DbNFoD8HbYQ93W8SOo` and the relevant node ID to see what the page should look like.
68
+ 2. **Test the running app with Playwright** — Use the full Playwright MCP toolset to:
69
+ - Navigate to pages (`mcp__playwright__browser_navigate`)
70
+ - Take snapshots (`mcp__playwright__browser_snapshot`) and screenshots (`mcp__playwright__browser_take_screenshot`)
71
+ - Click elements, fill forms, and interact with the UI
72
+ - Check console for errors (`mcp__playwright__browser_console_messages`)
73
+ - Inspect network requests (`mcp__playwright__browser_network_requests`)
74
+ 3. **Compare Figma vs live app** — Take a Figma screenshot and a Playwright screenshot, then verify layout, colors, text, and interactions match the design.
75
+ 4. **Test SSE updates** — The dashboard uses server-sent events for live data. Verify that agent status, queues, and event log update in real time.
@@ -0,0 +1,17 @@
1
+ # IDEAS
2
+
3
+ ## Features
4
+
5
+ - **RAG MCP server for agent context** — pre-indexed codebase so fresh agents skip discovery phase. Biggest win for repeated spawns against same repo.
6
+ - **Resume on input** — when an agent needs user input, it exits and moves task to "Needs Input" queue. User answers in comments, moves to "Received Input". Dispatcher route picks it up and spawns with `--resume <session_id>`. No dispatcher changes needed if `prompt_fn` can read session_id from task metadata.
7
+ - **Shared input board** — single board across projects where blocked tasks collect. User has one inbox for all agent questions. Tasks route back to original board after input received.
8
+ - **Soften stale-slot reaper** — once root cause of slot leaks is identified, change from crash-on-detection to cleanup-and-continue.
9
+
10
+ ## Patterns to test and document
11
+
12
+ - **Resume-on-input** — agent asks question via comment, moves to input queue, exits. Fresh agent resumes conversation when input received. Test: does `--resume` with full comment history produce good results vs fresh spawn?
13
+ - **Research-then-implement pipeline** — research agent produces findings, implementation agent picks up from there. Two queues, two routes, task flows between them.
14
+ - **Priority-based queue draining** — high-priority queue (bugs) drains before low-priority (features) using route priority. Already implemented, needs documentation.
15
+ - **Per-queue poll intervals** — hot queues poll frequently, cold queues poll rarely. Already implemented, needs documentation.
16
+ - **Retry with dead-letter queue** — failed tasks retry N times then move to DLQ for manual review. Already implemented, needs documentation.
17
+ - **Asset downloading via adapter** — adapter downloads task attachments/images from backend at spawn time, saves to temp folder, agent receives local paths. Keeps backend auth and API details out of agents.
athanore-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scott
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,437 @@
1
+ Metadata-Version: 2.4
2
+ Name: athanore
3
+ Version: 0.0.1
4
+ Summary: Polls project management APIs for ready tickets and spawns AI agents to work on them
5
+ Author-email: Scott <me@scottrussell.net>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: agent,ai,automation,dispatcher
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.13
15
+ Requires-Dist: starlette>=0.30.0
16
+ Requires-Dist: uvicorn>=0.20.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # athanore
20
+
21
+ Polls task queues, dispatches agent subprocesses, and exposes an HTTP API so agents can interact with tasks without knowing which backend is in use.
22
+
23
+ ## How it works
24
+
25
+ The router polls configured queues for ready tasks. When it finds one, it moves the task to an in-progress queue, spawns an agent (via an `AgentAdapter`), and passes task details to the agent. The subprocess uses a local HTTP API to read task details, post comments, update fields, and move the task when done. The agent never talks to the backend directly.
26
+
27
+ ## Key concepts
28
+
29
+ - **Queue adapters** — Protocol-based (`QueueAdapter`, 11 methods). Ships with a JSON file adapter. A Planka adapter is included as a user-land example (`planka_backend.py`). Implement the protocol for anything else (Jira, Trello, Linear, SQLite, SQS, etc.).
30
+ - **Agent adapters** — Protocol-based (`AgentAdapter`). Owns the full agent run lifecycle (spawn, monitor, cleanup). Ships with `SubprocessAgentAdapter` (generic subprocess management). A Claude adapter using the `claude-code-sdk` is included as a user-land example (`claude_adapter.py`).
31
+ - **Routes** — Flask-style `@dispatcher.route()` decorators map queues to prompt-generating functions. Each route can specify its own agent adapter.
32
+ - **HTTP API** — Agents hit localhost. No credentials, no backend coupling.
33
+
34
+ ## Quick start
35
+
36
+ Requires Python 3.13+.
37
+
38
+ ```sh
39
+ uv pip install -e . # preferred (pip install -e . also works)
40
+ uv pip show athanore # verify the install succeeded
41
+ ```
42
+
43
+ Create a Python script (e.g. `run.py`):
44
+
45
+ ```python
46
+ from athanore import AgentDispatcher, JsonFileAdapter
47
+
48
+ dispatcher = AgentDispatcher(
49
+ command="claude",
50
+ poll_interval=30,
51
+ agent_timeout=600,
52
+ max_concurrent_agents=3,
53
+ queue_backend=JsonFileAdapter("/tmp/board.json"),
54
+ )
55
+
56
+ @dispatcher.route(
57
+ args=["--agent", "engineer", "-p"],
58
+ queue_name="Todo",
59
+ in_progress_queue="In Progress",
60
+ )
61
+ def engineer_agent(task_id: str, task_name: str) -> str:
62
+ return f"Work on task {task_id}: {task_name}."
63
+
64
+ if __name__ == "__main__":
65
+ dispatcher.run(debug=True) # enable DEBUG logging (default: False)
66
+ ```
67
+
68
+ > For Planka users, see `planka_backend.py` at the repo root for a ready-made backend.
69
+
70
+ ```sh
71
+ python run.py
72
+ ```
73
+
74
+ This starts two things:
75
+
76
+ 1. **Router** — polls configured queues, picks up tasks, moves them to in-progress, and spawns agents via agent adapters.
77
+ 2. **HTTP API** — listens on `http://{api_host}:{api_port}` so spawned agents can interact with tasks.
78
+
79
+ ## Configuration reference
80
+
81
+ All configuration is done via the `AgentDispatcher` constructor.
82
+
83
+ ### Constructor arguments
84
+
85
+ | Argument | Type | Default | Description |
86
+ |---|---|---|---|
87
+ | `command` | `str` | `""` | Base command to run for all routes (e.g. `"claude"`). Optional when using per-route `agent=` adapters. |
88
+ | `poll_interval` | `int` | `30` | Seconds between polls |
89
+ | `agent_timeout` | `int \| None` | `None` | Default timeout in seconds for all agents |
90
+ | `max_concurrent_agents` | `int` | `3` | Max agent processes at once |
91
+ | `api_host` | `str` | `"127.0.0.1"` | HTTP API bind address |
92
+ | `api_port` | `int` | `8000` | HTTP API port |
93
+ | `queue_backend` | `QueueAdapter \| None` | `None` | Task backend (required before calling `run()`). Any object with a `create_adapter()` method also works. |
94
+ | `default_agent` | `AgentAdapter \| None` | `None` | Default agent adapter for all routes. Defaults to `SubprocessAgentAdapter()` if not provided. |
95
+ | `max_retries` | `int` | `0` | Default max retry attempts for failed or timed-out agents. `0` disables retries. |
96
+ | `dead_letter_queue` | `str \| None` | `None` | Default queue name where tasks are moved after exhausting all retries. |
97
+ | `enable_queue_management` | `bool` | `False` | Enable queue CRUD HTTP endpoints |
98
+
99
+ ### Route decorator
100
+
101
+ The `@dispatcher.route()` decorator registers a queue-to-command mapping. The decorated function receives `(task_id, task_name)` and returns a prompt string appended to the command arguments.
102
+
103
+ ```python
104
+ @dispatcher.route(
105
+ queue_name="My Project.My Board.Todo", # required: queue to poll
106
+ in_progress_queue="My Project.My Board.WIP", # default: "In Progress"
107
+ args=["--agent", "engineer", "-p"], # extra args before prompt
108
+ timeout=1800, # route-specific timeout (optional)
109
+ poll_interval=10, # route-specific poll interval (optional)
110
+ priority=1, # dispatch priority (optional)
111
+ max_retries=3, # max retry attempts (optional)
112
+ dead_letter_queue="My Project.My Board.Failed", # DLQ for exhausted retries (optional)
113
+ agent=ClaudeAgentAdapter(subagent="eng"), # per-route agent adapter (optional)
114
+ )
115
+ def my_agent(task_id: str, task_name: str) -> str:
116
+ return f"Work on task {task_id}: {task_name}."
117
+ ```
118
+
119
+ ### Agent adapters
120
+
121
+ Agent adapters own the full lifecycle of running an agent: spawning, monitoring stdout, enforcing timeouts, and cleanup.
122
+
123
+ - **`SubprocessAgentAdapter`** — Generic subprocess management. Uses `route.format_command()` to build the command, spawns it, reads stdout, and enforces timeouts. Subclass to customize behavior.
124
+ - **`ClaudeAgentAdapter`** (`claude_adapter.py`) — Uses the `claude-code-sdk` to run Claude agents natively via the SDK's `query()` API. No subprocess management or stdout parsing needed. Picks up existing `.claude/` configuration automatically. Supports `subagent`, `model`, `permission_mode`, `max_turns`, `max_budget_usd`, and `cwd` constructor params.
125
+
126
+ Install the SDK separately: `uv pip install claude-code-sdk`
127
+
128
+ Per-route agent assignment:
129
+
130
+ ```python
131
+ from athanore import AgentDispatcher
132
+ from claude_adapter import ClaudeAgentAdapter
133
+
134
+ dispatcher = AgentDispatcher(
135
+ poll_interval=30,
136
+ queue_backend=my_backend,
137
+ )
138
+
139
+ @dispatcher.route(
140
+ queue_name="Engineering",
141
+ agent=ClaudeAgentAdapter(subagent="software-engineer"),
142
+ )
143
+ def eng(task_id, task_name):
144
+ return f"Implement {task_id}: {task_name}"
145
+
146
+ @dispatcher.route(
147
+ queue_name="Research",
148
+ agent=ClaudeAgentAdapter(subagent="research"),
149
+ )
150
+ def research(task_id, task_name):
151
+ return f"Research {task_id}: {task_name}"
152
+ ```
153
+
154
+ ### Agent timeouts
155
+
156
+ You can configure timeouts to automatically terminate agent processes that run too long:
157
+
158
+ - **`agent_timeout`** (constructor): Sets a global timeout in seconds for all agents. If not specified, agents run indefinitely.
159
+ - **`timeout`** (per-route): Sets a route-specific timeout in seconds. Overrides `agent_timeout` for that route.
160
+
161
+ When an agent times out:
162
+ 1. The process receives a TERM signal and has 5 seconds to exit gracefully
163
+ 2. If it doesn't exit, it receives a KILL signal
164
+ 3. A comment is added to the task noting the timeout
165
+
166
+ ```python
167
+ dispatcher = AgentDispatcher(
168
+ command="my-agent",
169
+ agent_timeout=3600, # 1 hour default for all agents
170
+ queue_backend=my_backend,
171
+ )
172
+
173
+ @dispatcher.route(
174
+ queue_name="Quick Tasks",
175
+ timeout=300, # 5 minutes for quick tasks (overrides default)
176
+ )
177
+ def quick(task_id, task_name):
178
+ return f"Handle {task_id}"
179
+
180
+ @dispatcher.route(queue_name="Long Tasks")
181
+ # No timeout — uses default of 3600 seconds
182
+ def long_running(task_id, task_name):
183
+ return f"Handle {task_id}"
184
+ ```
185
+
186
+ ### Route priority
187
+
188
+ When `max_concurrent_agents` is limited, routes with lower `priority` values are dispatched first. This lets you ensure downstream queues (closer to completion) are serviced before upstream ones, so a task flows all the way through a pipeline before new work begins.
189
+
190
+ Routes without an explicit `priority` use their registration order as a tiebreaker.
191
+
192
+ ```python
193
+ @dispatcher.route(queue_name="QA", priority=1) # serviced first
194
+ def qa(task_id, task_name):
195
+ return f"Review {task_id}"
196
+
197
+ @dispatcher.route(queue_name="Engineering", priority=2)
198
+ def eng(task_id, task_name):
199
+ return f"Implement {task_id}"
200
+
201
+ @dispatcher.route(queue_name="Todo", priority=3) # serviced last
202
+ def todo(task_id, task_name):
203
+ return f"Handle {task_id}"
204
+ ```
205
+
206
+ ### Per-queue poll intervals
207
+
208
+ By default, all queues are polled at the global `poll_interval` rate. You can override this per-route to poll high-priority queues more frequently or low-priority queues less often:
209
+
210
+ The router's internal tick rate automatically adjusts to the shortest configured interval, so no queue is ever starved.
211
+
212
+ ```python
213
+ dispatcher = AgentDispatcher(
214
+ command="my-agent",
215
+ poll_interval=60, # default for all queues
216
+ queue_backend=my_backend,
217
+ )
218
+
219
+ @dispatcher.route(queue_name="High Priority", poll_interval=10)
220
+ def urgent(task_id, task_name):
221
+ return f"Handle {task_id}"
222
+
223
+ @dispatcher.route(queue_name="Background", poll_interval=1800)
224
+ def background(task_id, task_name):
225
+ return f"Handle {task_id}"
226
+
227
+ @dispatcher.route(queue_name="Normal")
228
+ # No poll_interval — uses global default of 60s
229
+ def normal(task_id, task_name):
230
+ return f"Handle {task_id}"
231
+ ```
232
+
233
+ ### Retry and dead-letter queues
234
+
235
+ The dispatcher can automatically retry failed agents and optionally route exhausted tasks to a dead-letter queue (DLQ).
236
+
237
+ **What triggers a retry:**
238
+
239
+ - Agent failure (non-zero exit code)
240
+ - Agent timeout
241
+ - **Not** cancellation (e.g., router shutdown or `KeyboardInterrupt`)
242
+
243
+ **Retry behavior** — when a retry is triggered and the task's retry count is below `max_retries`:
244
+
245
+ 1. The task's retry count is incremented.
246
+ 2. A comment is added to the task (e.g., "Retry 1/3: timed out. Moving back to source queue for retry.").
247
+ 3. The task is moved back to the source queue (the original watched queue), where it will be picked up again on the next poll cycle.
248
+
249
+ **Exhaustion without DLQ** — when the retry count reaches `max_retries` and no `dead_letter_queue` is configured:
250
+
251
+ - A comment is added: "Max retries (N) exhausted. No dead-letter queue configured; leaving in place."
252
+ - The task stays in the in-progress queue. Manual intervention is required.
253
+
254
+ **Exhaustion with DLQ** — when the retry count reaches `max_retries` and a `dead_letter_queue` is configured:
255
+
256
+ - A comment is added: "Max retries (N) exhausted. Moving to dead-letter queue."
257
+ - The task is moved to the configured dead-letter queue.
258
+
259
+ **Configuration** — retry and DLQ settings can be configured at two levels:
260
+
261
+ - **Global defaults**: `max_retries` and `dead_letter_queue` on the `AgentDispatcher` constructor apply to all routes.
262
+ - **Per-route overrides**: `max_retries` and `dead_letter_queue` on `@dispatcher.route()` override the global defaults for that route.
263
+
264
+ > **Note:** Setting `dead_letter_queue=None` on a per-route basis does **not** disable a global DLQ — it falls through to the global default. There is currently no way to explicitly disable a globally configured DLQ for a single route.
265
+
266
+ ```python
267
+ dispatcher = AgentDispatcher(
268
+ command="my-agent",
269
+ max_retries=2, # default: retry up to 2 times
270
+ dead_letter_queue="Failed Tasks", # default DLQ for all routes
271
+ queue_backend=my_backend,
272
+ )
273
+
274
+ @dispatcher.route(
275
+ queue_name="Critical",
276
+ max_retries=5, # override: more retries for critical tasks
277
+ dead_letter_queue="Critical.Failed", # override: separate DLQ
278
+ )
279
+ def critical(task_id, task_name):
280
+ return f"Handle {task_id}"
281
+
282
+ @dispatcher.route(
283
+ queue_name="Best Effort",
284
+ max_retries=1, # override: only one retry
285
+ # No dead_letter_queue override — uses global "Failed Tasks"
286
+ )
287
+ def best_effort(task_id, task_name):
288
+ return f"Handle {task_id}"
289
+
290
+ @dispatcher.route(queue_name="Normal")
291
+ # No overrides — uses global defaults (2 retries, DLQ = "Failed Tasks")
292
+ def normal(task_id, task_name):
293
+ return f"Handle {task_id}"
294
+ ```
295
+
296
+ ## HTTP API
297
+
298
+ | Method | Endpoint | Description |
299
+ |---|---|---|
300
+ | `GET` | `/status` | Server metadata: capacity, timing, active agent count |
301
+ | `GET` | `/agents` | List running agents with task IDs, runtime, timeout |
302
+ | `GET` | `/agents/{task_id}` | Single agent detail with buffered event history |
303
+ | `GET` | `/tasks/{task_id}` | Full task info: description, labels, assignees, retry_count, comments |
304
+ | `GET` | `/tasks/{task_id}/events` | SSE stream of agent events for a task |
305
+ | `POST` | `/tasks/{task_id}/comments` | Post a comment on a task (`{"comment": "text"}`) |
306
+ | `POST` | `/tasks/{task_id}/move` | Move a task to a different queue (`{"target_queue": "name"}`) |
307
+ | `PATCH` | `/tasks/{task_id}` | Update task fields (`{"name": "...", "description": "...", "labels": [...], "assignees": [...]}`) |
308
+ | `POST` | `/tasks` | Create a new task (`{"queue_name": "...", "name": "...", "description": "..."}`) |
309
+ | `GET` | `/queues` | List all queues with task counts |
310
+ | `GET` | `/queues/{queue_name}` | Get details for a specific queue |
311
+ | `POST` | `/queues` | Create a new queue (`{"name": "..."}`) |
312
+ | `PATCH` | `/queues/{queue_name}` | Update/rename a queue (`{"name": "..."}`) |
313
+ | `DELETE` | `/queues/{queue_name}` | Delete an empty queue |
314
+ | `GET` | `/routes` | List all configured routes |
315
+ | `GET` | `/routes/{queue_name}` | Get details for a specific route |
316
+ | `POST` | `/routes` | Create a new route (`{"queue_name": "...", ...}`) |
317
+ | `PATCH` | `/routes/{queue_name}` | Update route fields |
318
+ | `DELETE` | `/routes/{queue_name}` | Delete a route |
319
+
320
+ Queue/route write endpoints (`POST`, `PATCH`, `DELETE`) require `enable_queue_management=True`.
321
+
322
+ ## Task lifecycle
323
+
324
+ 1. Task sits in a watched queue (e.g. `Todo`)
325
+ 2. Router picks it up, moves it to the in-progress queue, and assigns the authenticated user
326
+ 3. Router spawns an agent via the route's `AgentAdapter`
327
+ 4. The agent uses the HTTP API to read task details, add comments, etc.
328
+ 5. When finished, the agent calls the move endpoint to move the task to a done queue
329
+
330
+ ## Backends
331
+
332
+ ### Planka
333
+
334
+ The Planka backend is provided as a user-land file (`planka_backend.py` at the repo root), not as part of the library. It requires `plankapy>=2.3.0` to be installed separately:
335
+
336
+ ```sh
337
+ uv pip install plankapy>=2.3.0
338
+ ```
339
+
340
+ Uses dot-notation for queue naming: `Project.Board.List`.
341
+
342
+ ```python
343
+ from athanore import AgentDispatcher
344
+ from planka_backend import PlankaBackend
345
+
346
+ dispatcher = AgentDispatcher(
347
+ command="my-agent",
348
+ queue_backend=PlankaBackend(url="http://localhost:1337"),
349
+ )
350
+
351
+ @dispatcher.route(
352
+ queue_name="My Project.My Board.Todo",
353
+ in_progress_queue="My Project.My Board.In Progress",
354
+ args=["-p"],
355
+ )
356
+ def handle(task_id: str, task_name: str) -> str:
357
+ return f"Work on task {task_id}: {task_name}"
358
+ ```
359
+
360
+ #### Planka authentication
361
+
362
+ Credentials can be passed directly as kwargs or resolved from environment variables:
363
+
364
+ ```python
365
+ # Option 1: API token (kwarg)
366
+ PlankaBackend(url="http://localhost:3000", token="your-token-here")
367
+
368
+ # Option 2: Username + password (kwargs)
369
+ PlankaBackend(url="http://localhost:3000", username="admin", password="secret")
370
+
371
+ # Option 3: Environment variables (default when no kwargs are given)
372
+ # PLANKA_TOKEN=your-token-here
373
+ # — or —
374
+ # PLANKA_USER=admin + PLANKA_PASSWORD=secret
375
+ PlankaBackend(url="http://localhost:3000")
376
+ ```
377
+
378
+ Credentials are resolved at `dispatcher.run()` time, not at import time. If you use `.env` files, call `dotenv.load_dotenv()` in your script before `dispatcher.run()`.
379
+
380
+ ### JSON file
381
+
382
+ For development/testing or lightweight use without external services.
383
+
384
+ ```python
385
+ from athanore import AgentDispatcher, JsonFileAdapter
386
+
387
+ dispatcher = AgentDispatcher(
388
+ command="my-agent",
389
+ queue_backend=JsonFileAdapter("/tmp/board.json"),
390
+ )
391
+ ```
392
+
393
+ The JSON file structure:
394
+
395
+ ```json
396
+ {
397
+ "queues": {
398
+ "Todo": [
399
+ {"id": "1", "name": "Fix crash", "description": "...", "labels": [], "assignees": [], "comments": [], "tasks": []}
400
+ ],
401
+ "In Progress": [],
402
+ "Done": []
403
+ }
404
+ }
405
+ ```
406
+
407
+ ### Custom
408
+
409
+ Implement the `QueueAdapter` protocol (11 methods) in `athanore/queue_adapters/base.py`:
410
+
411
+ - `get_ready_tasks(queue_names)` — Return tasks from the given queues
412
+ - `get_task(task_id)` — Return a single task by ID
413
+ - `move_task(task_id, target_queue)` — Move a task between queues
414
+ - `add_comment(task_id, text)` — Add a comment to a task
415
+ - `update_task(task_id, *, assignees, name, description, labels)` — Update task fields
416
+ - `create_task(queue_name, name, description)` — Create a new task
417
+ - `list_queues()` — List all queues with task counts
418
+ - `get_queue(queue_name)` — Get a single queue's info
419
+ - `create_queue(queue_name)` — Create a new empty queue
420
+ - `update_queue(queue_name, *, new_name)` — Rename a queue
421
+ - `delete_queue(queue_name)` — Delete an empty queue
422
+
423
+ Pass your custom adapter directly to the constructor:
424
+
425
+ ```python
426
+ dispatcher = AgentDispatcher(
427
+ command="my-agent",
428
+ queue_backend=MyCustomAdapter(),
429
+ )
430
+ ```
431
+
432
+ ## Development
433
+
434
+ ```sh
435
+ uv pip install -e ".[dev]" # pip install -e ".[dev]" also works
436
+ pytest
437
+ ```