taskswarm-cli 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.
- taskswarm_cli-0.1.0/.gitignore +8 -0
- taskswarm_cli-0.1.0/LICENSE +21 -0
- taskswarm_cli-0.1.0/PKG-INFO +217 -0
- taskswarm_cli-0.1.0/README.md +182 -0
- taskswarm_cli-0.1.0/examples/01-basic-server/run.py +64 -0
- taskswarm_cli-0.1.0/examples/02-ci-gate/gate.py +70 -0
- taskswarm_cli-0.1.0/examples/03-claude-code-hook/relay.py +73 -0
- taskswarm_cli-0.1.0/examples/README.md +29 -0
- taskswarm_cli-0.1.0/pyproject.toml +67 -0
- taskswarm_cli-0.1.0/src/taskswarm/__init__.py +67 -0
- taskswarm_cli-0.1.0/src/taskswarm/adapters/__init__.py +11 -0
- taskswarm_cli-0.1.0/src/taskswarm/adapters/claude_code_adapter.py +226 -0
- taskswarm_cli-0.1.0/src/taskswarm/adapters/generic_adapter.py +43 -0
- taskswarm_cli-0.1.0/src/taskswarm/adapters/types.py +23 -0
- taskswarm_cli-0.1.0/src/taskswarm/cli.py +323 -0
- taskswarm_cli-0.1.0/src/taskswarm/client/__init__.py +0 -0
- taskswarm_cli-0.1.0/src/taskswarm/client/api_client.py +61 -0
- taskswarm_cli-0.1.0/src/taskswarm/client/tasks_registry.py +119 -0
- taskswarm_cli-0.1.0/src/taskswarm/notifications/__init__.py +3 -0
- taskswarm_cli-0.1.0/src/taskswarm/notifications/dispatch.py +88 -0
- taskswarm_cli-0.1.0/src/taskswarm/notifications/ntfy.py +61 -0
- taskswarm_cli-0.1.0/src/taskswarm/notifications/os_notify.py +61 -0
- taskswarm_cli-0.1.0/src/taskswarm/py.typed +0 -0
- taskswarm_cli-0.1.0/src/taskswarm/schema/__init__.py +23 -0
- taskswarm_cli-0.1.0/src/taskswarm/schema/events.py +221 -0
- taskswarm_cli-0.1.0/src/taskswarm/server/__init__.py +33 -0
- taskswarm_cli-0.1.0/src/taskswarm/server/auth.py +25 -0
- taskswarm_cli-0.1.0/src/taskswarm/server/config.py +154 -0
- taskswarm_cli-0.1.0/src/taskswarm/server/event_store.py +139 -0
- taskswarm_cli-0.1.0/src/taskswarm/server/http_server.py +255 -0
- taskswarm_cli-0.1.0/src/taskswarm/server/server.py +72 -0
- taskswarm_cli-0.1.0/src/taskswarm/ui/index.html +241 -0
- taskswarm_cli-0.1.0/src/taskswarm/util/__init__.py +0 -0
- taskswarm_cli-0.1.0/src/taskswarm/util/sync_sleep.py +15 -0
- taskswarm_cli-0.1.0/tests/__init__.py +0 -0
- taskswarm_cli-0.1.0/tests/conftest.py +14 -0
- taskswarm_cli-0.1.0/tests/test_adapters.py +200 -0
- taskswarm_cli-0.1.0/tests/test_api_client.py +80 -0
- taskswarm_cli-0.1.0/tests/test_auth.py +33 -0
- taskswarm_cli-0.1.0/tests/test_cli.py +138 -0
- taskswarm_cli-0.1.0/tests/test_config.py +110 -0
- taskswarm_cli-0.1.0/tests/test_event_store.py +112 -0
- taskswarm_cli-0.1.0/tests/test_events_schema.py +147 -0
- taskswarm_cli-0.1.0/tests/test_notify.py +103 -0
- taskswarm_cli-0.1.0/tests/test_ntfy.py +52 -0
- taskswarm_cli-0.1.0/tests/test_os_notify.py +20 -0
- taskswarm_cli-0.1.0/tests/test_server_lifecycle.py +179 -0
- taskswarm_cli-0.1.0/tests/test_tasks_registry.py +72 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rudrendu Paul
|
|
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,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taskswarm-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-hosted event server that pushes a notification the instant a parallel coding-agent session blocks, needs review, fails, or finishes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/RudrenduPaul/taskswarm
|
|
6
|
+
Project-URL: Repository, https://github.com/RudrenduPaul/taskswarm
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/RudrenduPaul/taskswarm/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/RudrenduPaul/taskswarm/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Documentation, https://github.com/RudrenduPaul/taskswarm/blob/main/docs/getting-started.md
|
|
10
|
+
Project-URL: Author - Rudrendu Paul, https://github.com/RudrenduPaul
|
|
11
|
+
Project-URL: Author - Sourav Nandy, https://github.com/Sourav-nandy-ai
|
|
12
|
+
Author: Rudrendu Paul, Sourav Nandy
|
|
13
|
+
License-Expression: MIT
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Keywords: claude-code,cli,coding-agent,dashboard,event-server,multi-agent,notifications,orchestration,self-hosted
|
|
16
|
+
Classifier: Development Status :: 3 - Alpha
|
|
17
|
+
Classifier: Environment :: Console
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
28
|
+
Classifier: Topic :: System :: Monitoring
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build<2,>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest<9,>=7.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine<7,>=5.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# taskswarm (Python)
|
|
37
|
+
|
|
38
|
+
Self-hosted event server that pushes a notification the instant a parallel
|
|
39
|
+
coding-agent session blocks, needs review, fails, or finishes.
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/taskswarm/)
|
|
42
|
+
[](https://github.com/RudrenduPaul/taskswarm/blob/main/LICENSE)
|
|
43
|
+
[](https://pypi.org/project/taskswarm/)
|
|
44
|
+
[](https://github.com/RudrenduPaul/taskswarm/actions/workflows/ci.yml)
|
|
45
|
+
|
|
46
|
+
## Why this exists
|
|
47
|
+
|
|
48
|
+
Running three or four coding-agent sessions in parallel turns into a
|
|
49
|
+
tab-switching problem: nothing pushes state to you, so you find out a
|
|
50
|
+
session has been sitting on a permission prompt for ten minutes only by
|
|
51
|
+
polling terminals by eye. TaskSwarm closes that gap: every agent session
|
|
52
|
+
reports its state to a small local HTTP server, and the instant one
|
|
53
|
+
transitions to `blocked`, `needs-review`, `failed`, or `done`, TaskSwarm
|
|
54
|
+
fires a local OS notification and updates a live status page over
|
|
55
|
+
Server-Sent Events. No polling, no account, no cloud dependency by default.
|
|
56
|
+
|
|
57
|
+
This package is the **Python distribution** of TaskSwarm -- a genuine,
|
|
58
|
+
independent port of the server, CLI, event schema, and notification logic,
|
|
59
|
+
not a wrapper around the Node binary. It has zero runtime dependencies: no
|
|
60
|
+
HTTP framework, no schema-validation library, nothing beyond the Python
|
|
61
|
+
standard library.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
Live on PyPI as `taskswarm`:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install taskswarm
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
or with [uv](https://docs.astral.sh/uv/):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv add taskswarm
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The complementary JS/TS distribution is already live on npm as
|
|
78
|
+
`taskswarm-cli` (`npm install -g taskswarm-cli`, or `npx taskswarm-cli
|
|
79
|
+
start` with no install step) -- see the
|
|
80
|
+
[project README](https://github.com/RudrenduPaul/taskswarm#readme) for
|
|
81
|
+
that package. Both are first-class and maintained together; neither is a
|
|
82
|
+
replacement for the other.
|
|
83
|
+
|
|
84
|
+
## Quickstart
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Terminal 1: start the server
|
|
88
|
+
taskswarm start
|
|
89
|
+
# TaskSwarm server listening on http://127.0.0.1:4173
|
|
90
|
+
# Live status page: http://127.0.0.1:4173/?token=<your-token>
|
|
91
|
+
|
|
92
|
+
# Terminal 2: report a session's status as it works
|
|
93
|
+
taskswarm agent report-status --task my-fix --repo ./api --state running
|
|
94
|
+
taskswarm agent report-status --task my-fix --repo ./api --state done
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Open the live status page URL printed by `start`. The row for `my-fix`
|
|
98
|
+
updates the instant each `report-status` call lands, no refresh.
|
|
99
|
+
|
|
100
|
+
Or call the library directly, in-process, without a subprocess:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from taskswarm import start_server
|
|
104
|
+
from taskswarm.adapters import GenericAdapter
|
|
105
|
+
from taskswarm.client.api_client import post_event
|
|
106
|
+
|
|
107
|
+
running = start_server()
|
|
108
|
+
adapter = GenericAdapter()
|
|
109
|
+
event_input = adapter.to_event_input(
|
|
110
|
+
{"session_id": "my-fix", "repo": "./api", "status": "done", "agent_type": "generic"}
|
|
111
|
+
)
|
|
112
|
+
event = post_event(running.config.to_dict(), event_input)
|
|
113
|
+
print(event["session_id"], "->", event["status"])
|
|
114
|
+
running.close()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## What it does
|
|
118
|
+
|
|
119
|
+
- **Event server, not a board you have to keep open.** `taskswarm start`
|
|
120
|
+
boots an HTTP+SSE server (stdlib `http.server`, no framework dependency)
|
|
121
|
+
that accepts `POST /events`, serves `GET /events` for current session
|
|
122
|
+
state, and streams new events over `GET /live` (Server-Sent Events) to
|
|
123
|
+
the bundled live status page.
|
|
124
|
+
- **Push notification on the four states that mean "look at this now."**
|
|
125
|
+
The moment a session's status transitions to `blocked`, `needs-review`,
|
|
126
|
+
`failed`, or `done`, TaskSwarm fires a native OS notification
|
|
127
|
+
(`osascript` on macOS, a terminal-bell console fallback elsewhere).
|
|
128
|
+
Notification dedup keys on the `(status, blocked_reason)` pair, so a
|
|
129
|
+
second, different permission prompt still notifies even though the
|
|
130
|
+
status (`needs-review`) didn't change.
|
|
131
|
+
- **A wrapper-script adapter for any agent.**
|
|
132
|
+
`taskswarm agent report-status --task <id> --repo <path> --state <state>`
|
|
133
|
+
is the one primitive every integration is built on -- callable from any
|
|
134
|
+
script wrapping any CLI agent (Codex, Cursor, or anything else).
|
|
135
|
+
- **A Claude Code hooks adapter**, ported with the same behavior as the npm
|
|
136
|
+
package's: `taskswarm hooks install claude-code` writes `Stop` and
|
|
137
|
+
`Notification` hook entries into `.claude/settings.json`, pointed at the
|
|
138
|
+
exact, already-installed console script on disk (never a floating PATH
|
|
139
|
+
lookup re-resolved on every hook fire).
|
|
140
|
+
- **A bearer-token-gated local API, bound to loopback by default.**
|
|
141
|
+
`POST /events` and the live page both require the token TaskSwarm
|
|
142
|
+
generates on first run (`~/.taskswarm/config.json`, written `0600`).
|
|
143
|
+
Rotate it with `taskswarm token rotate`.
|
|
144
|
+
- **Agent-native by design.** Every subcommand ships a `--json` flag with a
|
|
145
|
+
stable schema, including error output.
|
|
146
|
+
- **ntfy.sh is opt-in, never default.** The only notification channel that
|
|
147
|
+
leaves your machine, and it's off unless you configure it in
|
|
148
|
+
`~/.taskswarm/config.json`.
|
|
149
|
+
|
|
150
|
+
## CLI reference
|
|
151
|
+
|
|
152
|
+
| Command | Description | Key options |
|
|
153
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
154
|
+
| `taskswarm start` | Start the TaskSwarm server and print the live status page URL | `--json` |
|
|
155
|
+
| `taskswarm task add` | Register a new task | `--title <title>` (required), `--repo <path>` (required), `--json` |
|
|
156
|
+
| `taskswarm task list` | List tracked tasks, enriched with live status when the server is reachable | `--json` |
|
|
157
|
+
| `taskswarm agent report-status` | Report a status transition for a task/session to the local server | `--task <id>` (required), `--repo <path>` (required), `--state <state>` (required, one of `queued\|running\|blocked\|needs-review\|done\|failed`), `--blocked-reason <text>`, `--agent-type <type>` (`claude-code\|codex\|cursor\|generic`, default `generic`), `--json` |
|
|
158
|
+
| `taskswarm token rotate` | Generate a new bearer token, invalidating the old one | `--json` |
|
|
159
|
+
| `taskswarm hooks install <adapter>` | Install hooks for an agent integration (currently `claude-code`) | `--scope <project\|local\|user>` (default `project`), `--project-dir <path>`, `--json` |
|
|
160
|
+
| `taskswarm hooks claude-code-relay` | Internal: reads a hook payload from stdin and relays it. Installed automatically; not meant to be run by hand. | none |
|
|
161
|
+
|
|
162
|
+
`taskswarm --version` prints `taskswarm 0.1.0`. The `taskswarm-cli` console
|
|
163
|
+
script installed by this package is an identical alias, matching both `bin`
|
|
164
|
+
entries the npm package ships.
|
|
165
|
+
|
|
166
|
+
## How it works
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
agent session / hook / wrapper script
|
|
170
|
+
|
|
|
171
|
+
v
|
|
172
|
+
POST /events (bearer token required) -> event schema validation
|
|
173
|
+
|
|
|
174
|
+
v
|
|
175
|
+
EventStore (in-memory + append-only JSONL log at ~/.taskswarm/events.jsonl)
|
|
176
|
+
|
|
|
177
|
+
+--> notify() -- fires on a transition into blocked/needs-review/failed/done
|
|
178
|
+
| |
|
|
179
|
+
| +--> local OS notification (always on)
|
|
180
|
+
| +--> ntfy.sh (opt-in only)
|
|
181
|
+
|
|
|
182
|
+
+--> GET /live (Server-Sent Events) -> live status page
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Full data model and the exact notification-dedup rule are in
|
|
186
|
+
[docs/concepts.md](https://github.com/RudrenduPaul/taskswarm/blob/main/docs/concepts.md).
|
|
187
|
+
|
|
188
|
+
## Security
|
|
189
|
+
|
|
190
|
+
The local API is gated by a bearer token generated on first run and stored
|
|
191
|
+
`0600` at `~/.taskswarm/config.json`; the server binds to `127.0.0.1` by
|
|
192
|
+
default. Token comparison uses `hmac.compare_digest` (constant-time,
|
|
193
|
+
avoiding timing side-channels), the same property the TypeScript version
|
|
194
|
+
gets from `crypto.timingSafeEqual`. See
|
|
195
|
+
[SECURITY.md](https://github.com/RudrenduPaul/taskswarm/blob/main/SECURITY.md)
|
|
196
|
+
for the full posture, including what this server does **not** protect
|
|
197
|
+
against (it is a local developer tool, not designed to be exposed on a
|
|
198
|
+
shared or public network). **Honest note**: this project does not currently
|
|
199
|
+
publish SLSA provenance, Sigstore signatures, or an SBOM, and has no
|
|
200
|
+
OpenSSF Scorecard badge -- none of that infrastructure exists yet for
|
|
201
|
+
either distribution, so it isn't claimed here.
|
|
202
|
+
|
|
203
|
+
## Contributing
|
|
204
|
+
|
|
205
|
+
See [CONTRIBUTING.md](https://github.com/RudrenduPaul/taskswarm/blob/main/CONTRIBUTING.md)
|
|
206
|
+
for the full guide, covering both the TypeScript and Python codebases.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cd python
|
|
210
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
211
|
+
pip install -e ".[dev]"
|
|
212
|
+
pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT, see [LICENSE](https://github.com/RudrenduPaul/taskswarm/blob/main/LICENSE).
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# taskswarm (Python)
|
|
2
|
+
|
|
3
|
+
Self-hosted event server that pushes a notification the instant a parallel
|
|
4
|
+
coding-agent session blocks, needs review, fails, or finishes.
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/taskswarm/)
|
|
7
|
+
[](https://github.com/RudrenduPaul/taskswarm/blob/main/LICENSE)
|
|
8
|
+
[](https://pypi.org/project/taskswarm/)
|
|
9
|
+
[](https://github.com/RudrenduPaul/taskswarm/actions/workflows/ci.yml)
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
Running three or four coding-agent sessions in parallel turns into a
|
|
14
|
+
tab-switching problem: nothing pushes state to you, so you find out a
|
|
15
|
+
session has been sitting on a permission prompt for ten minutes only by
|
|
16
|
+
polling terminals by eye. TaskSwarm closes that gap: every agent session
|
|
17
|
+
reports its state to a small local HTTP server, and the instant one
|
|
18
|
+
transitions to `blocked`, `needs-review`, `failed`, or `done`, TaskSwarm
|
|
19
|
+
fires a local OS notification and updates a live status page over
|
|
20
|
+
Server-Sent Events. No polling, no account, no cloud dependency by default.
|
|
21
|
+
|
|
22
|
+
This package is the **Python distribution** of TaskSwarm -- a genuine,
|
|
23
|
+
independent port of the server, CLI, event schema, and notification logic,
|
|
24
|
+
not a wrapper around the Node binary. It has zero runtime dependencies: no
|
|
25
|
+
HTTP framework, no schema-validation library, nothing beyond the Python
|
|
26
|
+
standard library.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Live on PyPI as `taskswarm`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install taskswarm
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
or with [uv](https://docs.astral.sh/uv/):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add taskswarm
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The complementary JS/TS distribution is already live on npm as
|
|
43
|
+
`taskswarm-cli` (`npm install -g taskswarm-cli`, or `npx taskswarm-cli
|
|
44
|
+
start` with no install step) -- see the
|
|
45
|
+
[project README](https://github.com/RudrenduPaul/taskswarm#readme) for
|
|
46
|
+
that package. Both are first-class and maintained together; neither is a
|
|
47
|
+
replacement for the other.
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Terminal 1: start the server
|
|
53
|
+
taskswarm start
|
|
54
|
+
# TaskSwarm server listening on http://127.0.0.1:4173
|
|
55
|
+
# Live status page: http://127.0.0.1:4173/?token=<your-token>
|
|
56
|
+
|
|
57
|
+
# Terminal 2: report a session's status as it works
|
|
58
|
+
taskswarm agent report-status --task my-fix --repo ./api --state running
|
|
59
|
+
taskswarm agent report-status --task my-fix --repo ./api --state done
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Open the live status page URL printed by `start`. The row for `my-fix`
|
|
63
|
+
updates the instant each `report-status` call lands, no refresh.
|
|
64
|
+
|
|
65
|
+
Or call the library directly, in-process, without a subprocess:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from taskswarm import start_server
|
|
69
|
+
from taskswarm.adapters import GenericAdapter
|
|
70
|
+
from taskswarm.client.api_client import post_event
|
|
71
|
+
|
|
72
|
+
running = start_server()
|
|
73
|
+
adapter = GenericAdapter()
|
|
74
|
+
event_input = adapter.to_event_input(
|
|
75
|
+
{"session_id": "my-fix", "repo": "./api", "status": "done", "agent_type": "generic"}
|
|
76
|
+
)
|
|
77
|
+
event = post_event(running.config.to_dict(), event_input)
|
|
78
|
+
print(event["session_id"], "->", event["status"])
|
|
79
|
+
running.close()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## What it does
|
|
83
|
+
|
|
84
|
+
- **Event server, not a board you have to keep open.** `taskswarm start`
|
|
85
|
+
boots an HTTP+SSE server (stdlib `http.server`, no framework dependency)
|
|
86
|
+
that accepts `POST /events`, serves `GET /events` for current session
|
|
87
|
+
state, and streams new events over `GET /live` (Server-Sent Events) to
|
|
88
|
+
the bundled live status page.
|
|
89
|
+
- **Push notification on the four states that mean "look at this now."**
|
|
90
|
+
The moment a session's status transitions to `blocked`, `needs-review`,
|
|
91
|
+
`failed`, or `done`, TaskSwarm fires a native OS notification
|
|
92
|
+
(`osascript` on macOS, a terminal-bell console fallback elsewhere).
|
|
93
|
+
Notification dedup keys on the `(status, blocked_reason)` pair, so a
|
|
94
|
+
second, different permission prompt still notifies even though the
|
|
95
|
+
status (`needs-review`) didn't change.
|
|
96
|
+
- **A wrapper-script adapter for any agent.**
|
|
97
|
+
`taskswarm agent report-status --task <id> --repo <path> --state <state>`
|
|
98
|
+
is the one primitive every integration is built on -- callable from any
|
|
99
|
+
script wrapping any CLI agent (Codex, Cursor, or anything else).
|
|
100
|
+
- **A Claude Code hooks adapter**, ported with the same behavior as the npm
|
|
101
|
+
package's: `taskswarm hooks install claude-code` writes `Stop` and
|
|
102
|
+
`Notification` hook entries into `.claude/settings.json`, pointed at the
|
|
103
|
+
exact, already-installed console script on disk (never a floating PATH
|
|
104
|
+
lookup re-resolved on every hook fire).
|
|
105
|
+
- **A bearer-token-gated local API, bound to loopback by default.**
|
|
106
|
+
`POST /events` and the live page both require the token TaskSwarm
|
|
107
|
+
generates on first run (`~/.taskswarm/config.json`, written `0600`).
|
|
108
|
+
Rotate it with `taskswarm token rotate`.
|
|
109
|
+
- **Agent-native by design.** Every subcommand ships a `--json` flag with a
|
|
110
|
+
stable schema, including error output.
|
|
111
|
+
- **ntfy.sh is opt-in, never default.** The only notification channel that
|
|
112
|
+
leaves your machine, and it's off unless you configure it in
|
|
113
|
+
`~/.taskswarm/config.json`.
|
|
114
|
+
|
|
115
|
+
## CLI reference
|
|
116
|
+
|
|
117
|
+
| Command | Description | Key options |
|
|
118
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
119
|
+
| `taskswarm start` | Start the TaskSwarm server and print the live status page URL | `--json` |
|
|
120
|
+
| `taskswarm task add` | Register a new task | `--title <title>` (required), `--repo <path>` (required), `--json` |
|
|
121
|
+
| `taskswarm task list` | List tracked tasks, enriched with live status when the server is reachable | `--json` |
|
|
122
|
+
| `taskswarm agent report-status` | Report a status transition for a task/session to the local server | `--task <id>` (required), `--repo <path>` (required), `--state <state>` (required, one of `queued\|running\|blocked\|needs-review\|done\|failed`), `--blocked-reason <text>`, `--agent-type <type>` (`claude-code\|codex\|cursor\|generic`, default `generic`), `--json` |
|
|
123
|
+
| `taskswarm token rotate` | Generate a new bearer token, invalidating the old one | `--json` |
|
|
124
|
+
| `taskswarm hooks install <adapter>` | Install hooks for an agent integration (currently `claude-code`) | `--scope <project\|local\|user>` (default `project`), `--project-dir <path>`, `--json` |
|
|
125
|
+
| `taskswarm hooks claude-code-relay` | Internal: reads a hook payload from stdin and relays it. Installed automatically; not meant to be run by hand. | none |
|
|
126
|
+
|
|
127
|
+
`taskswarm --version` prints `taskswarm 0.1.0`. The `taskswarm-cli` console
|
|
128
|
+
script installed by this package is an identical alias, matching both `bin`
|
|
129
|
+
entries the npm package ships.
|
|
130
|
+
|
|
131
|
+
## How it works
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
agent session / hook / wrapper script
|
|
135
|
+
|
|
|
136
|
+
v
|
|
137
|
+
POST /events (bearer token required) -> event schema validation
|
|
138
|
+
|
|
|
139
|
+
v
|
|
140
|
+
EventStore (in-memory + append-only JSONL log at ~/.taskswarm/events.jsonl)
|
|
141
|
+
|
|
|
142
|
+
+--> notify() -- fires on a transition into blocked/needs-review/failed/done
|
|
143
|
+
| |
|
|
144
|
+
| +--> local OS notification (always on)
|
|
145
|
+
| +--> ntfy.sh (opt-in only)
|
|
146
|
+
|
|
|
147
|
+
+--> GET /live (Server-Sent Events) -> live status page
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Full data model and the exact notification-dedup rule are in
|
|
151
|
+
[docs/concepts.md](https://github.com/RudrenduPaul/taskswarm/blob/main/docs/concepts.md).
|
|
152
|
+
|
|
153
|
+
## Security
|
|
154
|
+
|
|
155
|
+
The local API is gated by a bearer token generated on first run and stored
|
|
156
|
+
`0600` at `~/.taskswarm/config.json`; the server binds to `127.0.0.1` by
|
|
157
|
+
default. Token comparison uses `hmac.compare_digest` (constant-time,
|
|
158
|
+
avoiding timing side-channels), the same property the TypeScript version
|
|
159
|
+
gets from `crypto.timingSafeEqual`. See
|
|
160
|
+
[SECURITY.md](https://github.com/RudrenduPaul/taskswarm/blob/main/SECURITY.md)
|
|
161
|
+
for the full posture, including what this server does **not** protect
|
|
162
|
+
against (it is a local developer tool, not designed to be exposed on a
|
|
163
|
+
shared or public network). **Honest note**: this project does not currently
|
|
164
|
+
publish SLSA provenance, Sigstore signatures, or an SBOM, and has no
|
|
165
|
+
OpenSSF Scorecard badge -- none of that infrastructure exists yet for
|
|
166
|
+
either distribution, so it isn't claimed here.
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
See [CONTRIBUTING.md](https://github.com/RudrenduPaul/taskswarm/blob/main/CONTRIBUTING.md)
|
|
171
|
+
for the full guide, covering both the TypeScript and Python codebases.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
cd python
|
|
175
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
176
|
+
pip install -e ".[dev]"
|
|
177
|
+
pytest
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT, see [LICENSE](https://github.com/RudrenduPaul/taskswarm/blob/main/LICENSE).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Boots a real TaskSwarm server on an ephemeral port, reports a session
|
|
4
|
+
through queued -> running -> done using the GenericAdapter, reads back live
|
|
5
|
+
session state, and confirms a local notification fired on the transition
|
|
6
|
+
into 'done' (one of the four states TaskSwarm's notification layer watches
|
|
7
|
+
for). Run it directly:
|
|
8
|
+
|
|
9
|
+
python3 examples/01-basic-server/run.py
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from taskswarm.adapters import GenericAdapter
|
|
16
|
+
from taskswarm.client.api_client import get_sessions, post_event
|
|
17
|
+
from taskswarm.notifications import NotifyOptions
|
|
18
|
+
from taskswarm.server.config import TaskSwarmConfig, generate_token
|
|
19
|
+
from taskswarm.server.server import start_server
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
fired = []
|
|
24
|
+
|
|
25
|
+
def on_notify(title: str, message: str) -> None:
|
|
26
|
+
fired.append((title, message))
|
|
27
|
+
|
|
28
|
+
# An ephemeral, isolated home dir -- this example never touches your
|
|
29
|
+
# real ~/.taskswarm.
|
|
30
|
+
home = tempfile.mkdtemp(prefix="taskswarm-example-")
|
|
31
|
+
print(f"Using TASKSWARM_HOME={home}")
|
|
32
|
+
|
|
33
|
+
config = TaskSwarmConfig(token=generate_token(), port=0, host="127.0.0.1")
|
|
34
|
+
running = start_server(
|
|
35
|
+
config=config,
|
|
36
|
+
log_path=str(Path(home) / "events.jsonl"),
|
|
37
|
+
notify_options=NotifyOptions(os_notifier=on_notify),
|
|
38
|
+
)
|
|
39
|
+
print(f"Server listening at {running.url}")
|
|
40
|
+
|
|
41
|
+
adapter = GenericAdapter()
|
|
42
|
+
|
|
43
|
+
for status in ("queued", "running", "done"):
|
|
44
|
+
event_input = adapter.to_event_input(
|
|
45
|
+
{"session_id": "example-task", "repo": "./demo-repo", "status": status, "agent_type": "generic"}
|
|
46
|
+
)
|
|
47
|
+
event = post_event(running.config.to_dict(), event_input)
|
|
48
|
+
print(f"reported: {event['session_id']} -> {event['status']}")
|
|
49
|
+
|
|
50
|
+
sessions = get_sessions(running.config.to_dict())
|
|
51
|
+
print("\ncurrent session state:")
|
|
52
|
+
print(json.dumps(sessions, indent=2))
|
|
53
|
+
|
|
54
|
+
print(f"\nnotifications fired: {len(fired)}")
|
|
55
|
+
for title, message in fired:
|
|
56
|
+
print(f" [{title}] {message}")
|
|
57
|
+
|
|
58
|
+
assert len(fired) == 1, "expected exactly one notification, for the 'done' transition"
|
|
59
|
+
running.close()
|
|
60
|
+
print("\nOK: server started, event reported, notification fired, server closed cleanly.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Demonstrates the CI-gate pattern documented in docs/integrations/ci.md:
|
|
4
|
+
report a task's outcome, poll session state, and exit non-zero if anything
|
|
5
|
+
failed -- the shape a real CI step would use, minus the real CI system.
|
|
6
|
+
|
|
7
|
+
Boots its own ephemeral server so this is runnable standalone. In a real
|
|
8
|
+
pipeline, drop the `start_server()` call and point `config` at an
|
|
9
|
+
already-running server's `~/.taskswarm/config.json` instead (see
|
|
10
|
+
`taskswarm.load_or_create_config()`).
|
|
11
|
+
|
|
12
|
+
python3 examples/02-ci-gate/gate.py
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from taskswarm.adapters import GenericAdapter
|
|
19
|
+
from taskswarm.client.api_client import get_sessions, post_event
|
|
20
|
+
from taskswarm.notifications import NotifyOptions
|
|
21
|
+
from taskswarm.server.config import TaskSwarmConfig, generate_token
|
|
22
|
+
from taskswarm.server.server import start_server
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_ci_gate(config: dict) -> int:
|
|
26
|
+
"""Returns the process exit code a real CI step would use: 0 if no
|
|
27
|
+
tracked session ended in 'failed', 1 otherwise."""
|
|
28
|
+
sessions = get_sessions(config)
|
|
29
|
+
failed = [s for s in sessions if s["latest"]["status"] == "failed"]
|
|
30
|
+
if failed:
|
|
31
|
+
for session in failed:
|
|
32
|
+
reason = session["latest"].get("blocked_reason", "no reason given")
|
|
33
|
+
print(f"FAILED: {session['session_id']} ({reason})", file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
print(f"{len(sessions)} session(s) tracked, none failed.")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
home = tempfile.mkdtemp(prefix="taskswarm-example-")
|
|
41
|
+
config = TaskSwarmConfig(token=generate_token(), port=0, host="127.0.0.1")
|
|
42
|
+
running = start_server(
|
|
43
|
+
config=config,
|
|
44
|
+
log_path=str(Path(home) / "events.jsonl"),
|
|
45
|
+
notify_options=NotifyOptions(os_notifier=lambda title, message: None),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
adapter = GenericAdapter()
|
|
49
|
+
# Simulate an agent task that failed partway through.
|
|
50
|
+
post_event(
|
|
51
|
+
running.config.to_dict(),
|
|
52
|
+
adapter.to_event_input(
|
|
53
|
+
{
|
|
54
|
+
"session_id": "flaky-test-fix",
|
|
55
|
+
"repo": "./demo-repo",
|
|
56
|
+
"status": "failed",
|
|
57
|
+
"agent_type": "generic",
|
|
58
|
+
"blocked_reason": "test suite still red after the agent's fix",
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
exit_code = run_ci_gate(running.config.to_dict())
|
|
64
|
+
running.close()
|
|
65
|
+
print(f"\nCI gate would exit with code {exit_code}")
|
|
66
|
+
sys.exit(exit_code)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
The agent-native use case: feeds a real Claude Code hook payload shape
|
|
4
|
+
(the same JSON shape Claude Code writes to a hook's stdin) through
|
|
5
|
+
ClaudeCodeAdapter in-process -- no CLI subprocess, no server needed for
|
|
6
|
+
this part -- and inspects the resulting event input plus the
|
|
7
|
+
notification-dedup decision (should_notify) directly.
|
|
8
|
+
|
|
9
|
+
python3 examples/03-claude-code-hook/relay.py
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
from taskswarm.adapters import ClaudeCodeAdapter
|
|
14
|
+
from taskswarm.notifications import should_notify
|
|
15
|
+
|
|
16
|
+
# A realistic 'Notification' hook payload -- the shape Claude Code writes
|
|
17
|
+
# to stdin when a permission prompt appears mid-session.
|
|
18
|
+
permission_prompt_payload = {
|
|
19
|
+
"session_id": "claude-session-42",
|
|
20
|
+
"cwd": "/Users/demo/projects/api",
|
|
21
|
+
"hook_event_name": "Notification",
|
|
22
|
+
"notification_type": "permission_prompt",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# A 'Stop' payload -- fires when Claude Code finishes responding to a turn.
|
|
26
|
+
stop_payload = {
|
|
27
|
+
"session_id": "claude-session-42",
|
|
28
|
+
"cwd": "/Users/demo/projects/api",
|
|
29
|
+
"hook_event_name": "Stop",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> None:
|
|
34
|
+
adapter = ClaudeCodeAdapter()
|
|
35
|
+
|
|
36
|
+
print("Permission-prompt hook -> event input:")
|
|
37
|
+
permission_event = adapter.to_event_input(permission_prompt_payload)
|
|
38
|
+
print(json.dumps(permission_event, indent=2))
|
|
39
|
+
|
|
40
|
+
print("\nStop hook -> event input:")
|
|
41
|
+
stop_event = adapter.to_event_input(stop_payload)
|
|
42
|
+
print(json.dumps(stop_event, indent=2))
|
|
43
|
+
|
|
44
|
+
# Notification-dedup: a first 'needs-review' with no prior status
|
|
45
|
+
# notifies. A second, identical-reason 'needs-review' right after does
|
|
46
|
+
# not (it's a repeat). A third 'needs-review' with a *different*
|
|
47
|
+
# blocked_reason (a different permission prompt) notifies again, even
|
|
48
|
+
# though the status itself hasn't changed.
|
|
49
|
+
print("\nNotification-dedup walkthrough:")
|
|
50
|
+
decisions = [
|
|
51
|
+
("first permission prompt", should_notify("needs-review", None, permission_event["blocked_reason"], None)),
|
|
52
|
+
(
|
|
53
|
+
"identical repeat",
|
|
54
|
+
should_notify(
|
|
55
|
+
"needs-review", "needs-review", permission_event["blocked_reason"], permission_event["blocked_reason"]
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
"a second, different prompt",
|
|
60
|
+
should_notify("needs-review", "needs-review", "a different reason entirely", permission_event["blocked_reason"]),
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
for label, decision in decisions:
|
|
64
|
+
print(f" {label}: should_notify = {decision}")
|
|
65
|
+
|
|
66
|
+
assert decisions[0][1] is True
|
|
67
|
+
assert decisions[1][1] is False
|
|
68
|
+
assert decisions[2][1] is True
|
|
69
|
+
print("\nOK: dedup decisions matched the documented rule (docs/concepts.md).")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|