taskmux 0.2.6__tar.gz → 0.2.7__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.
- {taskmux-0.2.6 → taskmux-0.2.7}/PKG-INFO +34 -6
- {taskmux-0.2.6 → taskmux-0.2.7}/README.md +33 -5
- {taskmux-0.2.6 → taskmux-0.2.7}/pyproject.toml +1 -1
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/agent.py +5 -2
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/cli.py +23 -23
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/tmux_manager.py +9 -13
- {taskmux-0.2.6 → taskmux-0.2.7}/.gitignore +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/LICENSE +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/__init__.py +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/config.py +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/daemon.py +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/hooks.py +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/init.py +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/main.py +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/models.py +0 -0
- {taskmux-0.2.6 → taskmux-0.2.7}/taskmux/templates/claude.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskmux
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Modern tmux-based task manager for LLM development tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/nc9/taskmux
|
|
6
6
|
Project-URL: Repository, https://github.com/nc9/taskmux
|
|
@@ -125,17 +125,22 @@ health_check = "test -f .migrate-complete"
|
|
|
125
125
|
[tasks.api]
|
|
126
126
|
command = "python manage.py runserver 0.0.0.0:8000"
|
|
127
127
|
cwd = "apps/api"
|
|
128
|
+
port = 8000
|
|
128
129
|
depends_on = ["migrate"]
|
|
129
130
|
health_check = "curl -sf http://localhost:8000/health"
|
|
131
|
+
stop_grace_period = 10
|
|
130
132
|
|
|
131
133
|
[tasks.worker]
|
|
132
134
|
command = "celery -A myapp worker -l info"
|
|
133
135
|
cwd = "apps/api"
|
|
134
136
|
depends_on = ["db"]
|
|
137
|
+
max_restarts = 3
|
|
138
|
+
restart_backoff = 3.0
|
|
135
139
|
|
|
136
140
|
[tasks.web]
|
|
137
141
|
command = "bun dev"
|
|
138
142
|
cwd = "apps/web"
|
|
143
|
+
port = 3000
|
|
139
144
|
depends_on = ["api"]
|
|
140
145
|
health_check = "curl -sf http://localhost:3000"
|
|
141
146
|
|
|
@@ -171,8 +176,8 @@ taskmux start storybook # Start a manual task
|
|
|
171
176
|
# Session
|
|
172
177
|
taskmux start # Start all auto_start tasks
|
|
173
178
|
taskmux start <task> # Start a single task
|
|
174
|
-
taskmux stop # Stop all tasks (
|
|
175
|
-
taskmux stop <task> # Stop a single task (
|
|
179
|
+
taskmux stop # Stop all tasks (C-c → SIGTERM → SIGKILL)
|
|
180
|
+
taskmux stop <task> # Stop a single task (signal escalation)
|
|
176
181
|
taskmux restart # Restart all tasks
|
|
177
182
|
taskmux restart <task> # Restart a single task
|
|
178
183
|
taskmux status # Show session status
|
|
@@ -206,8 +211,8 @@ taskmux daemon --port 8765 # Run with WebSocket API + auto-restart
|
|
|
206
211
|
|
|
207
212
|
### stop vs kill
|
|
208
213
|
|
|
209
|
-
- **`stop`** sends C-c
|
|
210
|
-
- **`kill`** destroys the window immediately.
|
|
214
|
+
- **`stop`** sends C-c, then escalates to SIGTERM → SIGKILL if the process doesn't exit within the grace period. Window stays alive so you can see exit output.
|
|
215
|
+
- **`kill`** kills the process group and destroys the window immediately.
|
|
211
216
|
|
|
212
217
|
## Configuration
|
|
213
218
|
|
|
@@ -226,7 +231,9 @@ after_stop = "echo done"
|
|
|
226
231
|
[tasks.server]
|
|
227
232
|
command = "python manage.py runserver"
|
|
228
233
|
cwd = "apps/api"
|
|
234
|
+
port = 8000
|
|
229
235
|
health_check = "curl -sf http://localhost:8000/health"
|
|
236
|
+
stop_grace_period = 10
|
|
230
237
|
depends_on = ["db"]
|
|
231
238
|
|
|
232
239
|
[tasks.server.hooks]
|
|
@@ -239,6 +246,7 @@ health_check = "pg_isready -h localhost"
|
|
|
239
246
|
[tasks.worker]
|
|
240
247
|
command = "celery worker -A myapp"
|
|
241
248
|
depends_on = ["db"]
|
|
249
|
+
max_restarts = 3
|
|
242
250
|
|
|
243
251
|
[tasks.tailwind]
|
|
244
252
|
command = "npx tailwindcss -w"
|
|
@@ -258,10 +266,14 @@ auto_start = false
|
|
|
258
266
|
| `tasks.<name>.command` | — | Shell command to run |
|
|
259
267
|
| `tasks.<name>.auto_start` | `true` | Start with `taskmux start` |
|
|
260
268
|
| `tasks.<name>.cwd` | — | Working directory for the task |
|
|
269
|
+
| `tasks.<name>.port` | — | Port to clean up before starting (kills orphaned listeners) |
|
|
261
270
|
| `tasks.<name>.health_check` | — | Shell command to check health (exit 0 = healthy) |
|
|
262
271
|
| `tasks.<name>.health_interval` | `10` | Seconds between health checks |
|
|
263
272
|
| `tasks.<name>.health_timeout` | `5` | Seconds before health check times out |
|
|
264
273
|
| `tasks.<name>.health_retries` | `3` | Consecutive failures before "unhealthy" |
|
|
274
|
+
| `tasks.<name>.stop_grace_period` | `5` | Seconds to wait after C-c before escalating to SIGTERM |
|
|
275
|
+
| `tasks.<name>.max_restarts` | `5` | Max auto-restarts in daemon mode before giving up (0 = unlimited) |
|
|
276
|
+
| `tasks.<name>.restart_backoff` | `2.0` | Multiplier for restart delay (1s, 2s, 4s, 8s… capped at 60s) |
|
|
265
277
|
| `tasks.<name>.depends_on` | `[]` | Task names that must be healthy before this task starts |
|
|
266
278
|
| `tasks.<name>.hooks.*` | — | Per-task lifecycle hooks (same fields as global) |
|
|
267
279
|
|
|
@@ -290,6 +302,20 @@ Hooks fire in this order:
|
|
|
290
302
|
|
|
291
303
|
If a `before_*` hook fails (non-zero exit), the action is aborted.
|
|
292
304
|
|
|
305
|
+
### Process Lifecycle
|
|
306
|
+
|
|
307
|
+
Taskmux ensures processes are fully stopped before restarting and that orphaned port listeners don't block new starts.
|
|
308
|
+
|
|
309
|
+
**Stop escalation** (`stop`, `restart`):
|
|
310
|
+
|
|
311
|
+
1. **C-c** (SIGINT) — waits `stop_grace_period` seconds (default 5)
|
|
312
|
+
2. **SIGTERM** to process group — waits 3 seconds
|
|
313
|
+
3. **SIGKILL** to process group — force kill
|
|
314
|
+
|
|
315
|
+
**Port cleanup** (`start`, `restart`): If `port` is configured, taskmux kills any process listening on that port before starting. This handles orphaned processes from crashed sessions.
|
|
316
|
+
|
|
317
|
+
**Auto-restart backoff** (daemon mode): When a task keeps crashing, restart delays increase exponentially (`restart_backoff` multiplier, capped at 60s). After `max_restarts` failures, the task is left stopped. The counter resets after 60 seconds of healthy uptime.
|
|
318
|
+
|
|
293
319
|
### Init & Agent Context
|
|
294
320
|
|
|
295
321
|
`taskmux init` bootstraps your project:
|
|
@@ -325,13 +351,15 @@ Use `--defaults` to skip prompts (CI/automation).
|
|
|
325
351
|
|
|
326
352
|
## Daemon Mode
|
|
327
353
|
|
|
328
|
-
Run as a background daemon with WebSocket API and auto-restart:
|
|
354
|
+
Run as a background daemon with WebSocket API and auto-restart with exponential backoff:
|
|
329
355
|
|
|
330
356
|
```bash
|
|
331
357
|
taskmux daemon # Default port 8765
|
|
332
358
|
taskmux daemon --port 9000 # Custom port
|
|
333
359
|
```
|
|
334
360
|
|
|
361
|
+
The daemon monitors task health every 30 seconds. Unhealthy tasks are restarted with exponential backoff (controlled by `restart_backoff` and `max_restarts`). Tasks that stay healthy for 60+ seconds have their restart counter reset.
|
|
362
|
+
|
|
335
363
|
WebSocket API:
|
|
336
364
|
|
|
337
365
|
```javascript
|
|
@@ -90,17 +90,22 @@ health_check = "test -f .migrate-complete"
|
|
|
90
90
|
[tasks.api]
|
|
91
91
|
command = "python manage.py runserver 0.0.0.0:8000"
|
|
92
92
|
cwd = "apps/api"
|
|
93
|
+
port = 8000
|
|
93
94
|
depends_on = ["migrate"]
|
|
94
95
|
health_check = "curl -sf http://localhost:8000/health"
|
|
96
|
+
stop_grace_period = 10
|
|
95
97
|
|
|
96
98
|
[tasks.worker]
|
|
97
99
|
command = "celery -A myapp worker -l info"
|
|
98
100
|
cwd = "apps/api"
|
|
99
101
|
depends_on = ["db"]
|
|
102
|
+
max_restarts = 3
|
|
103
|
+
restart_backoff = 3.0
|
|
100
104
|
|
|
101
105
|
[tasks.web]
|
|
102
106
|
command = "bun dev"
|
|
103
107
|
cwd = "apps/web"
|
|
108
|
+
port = 3000
|
|
104
109
|
depends_on = ["api"]
|
|
105
110
|
health_check = "curl -sf http://localhost:3000"
|
|
106
111
|
|
|
@@ -136,8 +141,8 @@ taskmux start storybook # Start a manual task
|
|
|
136
141
|
# Session
|
|
137
142
|
taskmux start # Start all auto_start tasks
|
|
138
143
|
taskmux start <task> # Start a single task
|
|
139
|
-
taskmux stop # Stop all tasks (
|
|
140
|
-
taskmux stop <task> # Stop a single task (
|
|
144
|
+
taskmux stop # Stop all tasks (C-c → SIGTERM → SIGKILL)
|
|
145
|
+
taskmux stop <task> # Stop a single task (signal escalation)
|
|
141
146
|
taskmux restart # Restart all tasks
|
|
142
147
|
taskmux restart <task> # Restart a single task
|
|
143
148
|
taskmux status # Show session status
|
|
@@ -171,8 +176,8 @@ taskmux daemon --port 8765 # Run with WebSocket API + auto-restart
|
|
|
171
176
|
|
|
172
177
|
### stop vs kill
|
|
173
178
|
|
|
174
|
-
- **`stop`** sends C-c
|
|
175
|
-
- **`kill`** destroys the window immediately.
|
|
179
|
+
- **`stop`** sends C-c, then escalates to SIGTERM → SIGKILL if the process doesn't exit within the grace period. Window stays alive so you can see exit output.
|
|
180
|
+
- **`kill`** kills the process group and destroys the window immediately.
|
|
176
181
|
|
|
177
182
|
## Configuration
|
|
178
183
|
|
|
@@ -191,7 +196,9 @@ after_stop = "echo done"
|
|
|
191
196
|
[tasks.server]
|
|
192
197
|
command = "python manage.py runserver"
|
|
193
198
|
cwd = "apps/api"
|
|
199
|
+
port = 8000
|
|
194
200
|
health_check = "curl -sf http://localhost:8000/health"
|
|
201
|
+
stop_grace_period = 10
|
|
195
202
|
depends_on = ["db"]
|
|
196
203
|
|
|
197
204
|
[tasks.server.hooks]
|
|
@@ -204,6 +211,7 @@ health_check = "pg_isready -h localhost"
|
|
|
204
211
|
[tasks.worker]
|
|
205
212
|
command = "celery worker -A myapp"
|
|
206
213
|
depends_on = ["db"]
|
|
214
|
+
max_restarts = 3
|
|
207
215
|
|
|
208
216
|
[tasks.tailwind]
|
|
209
217
|
command = "npx tailwindcss -w"
|
|
@@ -223,10 +231,14 @@ auto_start = false
|
|
|
223
231
|
| `tasks.<name>.command` | — | Shell command to run |
|
|
224
232
|
| `tasks.<name>.auto_start` | `true` | Start with `taskmux start` |
|
|
225
233
|
| `tasks.<name>.cwd` | — | Working directory for the task |
|
|
234
|
+
| `tasks.<name>.port` | — | Port to clean up before starting (kills orphaned listeners) |
|
|
226
235
|
| `tasks.<name>.health_check` | — | Shell command to check health (exit 0 = healthy) |
|
|
227
236
|
| `tasks.<name>.health_interval` | `10` | Seconds between health checks |
|
|
228
237
|
| `tasks.<name>.health_timeout` | `5` | Seconds before health check times out |
|
|
229
238
|
| `tasks.<name>.health_retries` | `3` | Consecutive failures before "unhealthy" |
|
|
239
|
+
| `tasks.<name>.stop_grace_period` | `5` | Seconds to wait after C-c before escalating to SIGTERM |
|
|
240
|
+
| `tasks.<name>.max_restarts` | `5` | Max auto-restarts in daemon mode before giving up (0 = unlimited) |
|
|
241
|
+
| `tasks.<name>.restart_backoff` | `2.0` | Multiplier for restart delay (1s, 2s, 4s, 8s… capped at 60s) |
|
|
230
242
|
| `tasks.<name>.depends_on` | `[]` | Task names that must be healthy before this task starts |
|
|
231
243
|
| `tasks.<name>.hooks.*` | — | Per-task lifecycle hooks (same fields as global) |
|
|
232
244
|
|
|
@@ -255,6 +267,20 @@ Hooks fire in this order:
|
|
|
255
267
|
|
|
256
268
|
If a `before_*` hook fails (non-zero exit), the action is aborted.
|
|
257
269
|
|
|
270
|
+
### Process Lifecycle
|
|
271
|
+
|
|
272
|
+
Taskmux ensures processes are fully stopped before restarting and that orphaned port listeners don't block new starts.
|
|
273
|
+
|
|
274
|
+
**Stop escalation** (`stop`, `restart`):
|
|
275
|
+
|
|
276
|
+
1. **C-c** (SIGINT) — waits `stop_grace_period` seconds (default 5)
|
|
277
|
+
2. **SIGTERM** to process group — waits 3 seconds
|
|
278
|
+
3. **SIGKILL** to process group — force kill
|
|
279
|
+
|
|
280
|
+
**Port cleanup** (`start`, `restart`): If `port` is configured, taskmux kills any process listening on that port before starting. This handles orphaned processes from crashed sessions.
|
|
281
|
+
|
|
282
|
+
**Auto-restart backoff** (daemon mode): When a task keeps crashing, restart delays increase exponentially (`restart_backoff` multiplier, capped at 60s). After `max_restarts` failures, the task is left stopped. The counter resets after 60 seconds of healthy uptime.
|
|
283
|
+
|
|
258
284
|
### Init & Agent Context
|
|
259
285
|
|
|
260
286
|
`taskmux init` bootstraps your project:
|
|
@@ -290,13 +316,15 @@ Use `--defaults` to skip prompts (CI/automation).
|
|
|
290
316
|
|
|
291
317
|
## Daemon Mode
|
|
292
318
|
|
|
293
|
-
Run as a background daemon with WebSocket API and auto-restart:
|
|
319
|
+
Run as a background daemon with WebSocket API and auto-restart with exponential backoff:
|
|
294
320
|
|
|
295
321
|
```bash
|
|
296
322
|
taskmux daemon # Default port 8765
|
|
297
323
|
taskmux daemon --port 9000 # Custom port
|
|
298
324
|
```
|
|
299
325
|
|
|
326
|
+
The daemon monitors task health every 30 seconds. Unhealthy tasks are restarted with exponential backoff (controlled by `restart_backoff` and `max_restarts`). Tasks that stay healthy for 60+ seconds have their restart counter reset.
|
|
327
|
+
|
|
300
328
|
WebSocket API:
|
|
301
329
|
|
|
302
330
|
```javascript
|
|
@@ -40,9 +40,12 @@ def buildContextBlock(config: TaskmuxConfig) -> str:
|
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
if config.tasks:
|
|
43
|
+
lines.append("| Task | Port | Auto-start | Command |")
|
|
44
|
+
lines.append("|------|------|------------|---------|")
|
|
43
45
|
for name, task in config.tasks.items():
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
port = str(task.port) if task.port else "—"
|
|
47
|
+
auto = "yes" if task.auto_start else "no"
|
|
48
|
+
lines.append(f"| {name} | {port} | {auto} | `{task.command}` |")
|
|
46
49
|
else:
|
|
47
50
|
lines.append('_No tasks configured yet. Use `taskmux add <name> "<command>"` to add._')
|
|
48
51
|
|
|
@@ -53,45 +53,41 @@ def init(
|
|
|
53
53
|
initProject(defaults=defaults)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
@app.command()
|
|
57
|
-
def list():
|
|
58
|
-
"""List all tasks and their status."""
|
|
59
|
-
cli = TaskmuxCLI()
|
|
60
|
-
cli.tmux.list_tasks()
|
|
61
|
-
|
|
62
|
-
|
|
63
56
|
@app.command()
|
|
64
57
|
def start(
|
|
65
|
-
|
|
58
|
+
tasks: list[str] = typer.Argument(None, help="Task names (omit for all)"), # noqa: B008
|
|
66
59
|
):
|
|
67
|
-
"""Start
|
|
60
|
+
"""Start tasks (all if none specified)."""
|
|
68
61
|
cli = TaskmuxCLI()
|
|
69
|
-
if
|
|
70
|
-
|
|
62
|
+
if tasks:
|
|
63
|
+
for task in tasks:
|
|
64
|
+
cli.tmux.start_task(task)
|
|
71
65
|
else:
|
|
72
66
|
cli.tmux.start_all()
|
|
73
67
|
|
|
74
68
|
|
|
75
69
|
@app.command()
|
|
76
70
|
def stop(
|
|
77
|
-
|
|
71
|
+
tasks: list[str] = typer.Argument(None, help="Task names (omit for all)"), # noqa: B008
|
|
78
72
|
):
|
|
79
|
-
"""Stop
|
|
73
|
+
"""Stop tasks (all if none specified)."""
|
|
80
74
|
cli = TaskmuxCLI()
|
|
81
|
-
if
|
|
82
|
-
|
|
75
|
+
if tasks:
|
|
76
|
+
for task in tasks:
|
|
77
|
+
cli.tmux.stop_task(task)
|
|
83
78
|
else:
|
|
84
79
|
cli.tmux.stop_all()
|
|
85
80
|
|
|
86
81
|
|
|
87
82
|
@app.command()
|
|
88
83
|
def restart(
|
|
89
|
-
|
|
84
|
+
tasks: list[str] = typer.Argument(None, help="Task names (omit for all)"), # noqa: B008
|
|
90
85
|
):
|
|
91
|
-
"""Restart
|
|
86
|
+
"""Restart tasks (all if none specified)."""
|
|
92
87
|
cli = TaskmuxCLI()
|
|
93
|
-
if
|
|
94
|
-
|
|
88
|
+
if tasks:
|
|
89
|
+
for task in tasks:
|
|
90
|
+
cli.tmux.restart_task(task)
|
|
95
91
|
else:
|
|
96
92
|
cli.tmux.restart_all()
|
|
97
93
|
|
|
@@ -160,11 +156,15 @@ def remove(
|
|
|
160
156
|
console.print(f"Task '{task}' not found in config", style="red")
|
|
161
157
|
|
|
162
158
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"""Show session status."""
|
|
159
|
+
def _status():
|
|
160
|
+
"""Show session and task status."""
|
|
166
161
|
cli = TaskmuxCLI()
|
|
167
|
-
cli.tmux.
|
|
162
|
+
cli.tmux.list_tasks()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
app.command(name="status")(_status)
|
|
166
|
+
app.command(name="list", hidden=True)(_status)
|
|
167
|
+
app.command(name="ls", hidden=True)(_status)
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
@app.command()
|
|
@@ -694,8 +694,12 @@ class TmuxManager:
|
|
|
694
694
|
console.print(f"[{color}]{prefix}[/{color}] {escape(line)}")
|
|
695
695
|
|
|
696
696
|
def list_tasks(self) -> None:
|
|
697
|
-
"""List all tasks and their status"""
|
|
698
|
-
|
|
697
|
+
"""List all tasks and their status."""
|
|
698
|
+
exists = self.session_exists()
|
|
699
|
+
print(f"Session '{self.config.name}': {'Running' if exists else 'Stopped'}")
|
|
700
|
+
if exists:
|
|
701
|
+
windows = self.list_windows()
|
|
702
|
+
print(f"Active tasks: {len(windows)}")
|
|
699
703
|
print("-" * 70)
|
|
700
704
|
|
|
701
705
|
if not self.config.tasks:
|
|
@@ -709,22 +713,14 @@ class TmuxManager:
|
|
|
709
713
|
"Healthy" if status["healthy"] else "Running" if status["running"] else "Stopped"
|
|
710
714
|
)
|
|
711
715
|
auto = "" if task_cfg.auto_start else " [manual]"
|
|
716
|
+
port = f" :{task_cfg.port}" if task_cfg.port else ""
|
|
712
717
|
extras = ""
|
|
713
718
|
if task_cfg.cwd:
|
|
714
719
|
extras += f" cwd={task_cfg.cwd}"
|
|
715
720
|
if task_cfg.depends_on:
|
|
716
721
|
extras += f" deps=[{','.join(task_cfg.depends_on)}]"
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
def show_status(self) -> None:
|
|
720
|
-
"""Show overall session status"""
|
|
721
|
-
exists = self.session_exists()
|
|
722
|
-
print(f"Session '{self.config.name}': {'Running' if exists else 'Stopped'} (libtmux)")
|
|
723
|
-
|
|
724
|
-
if exists:
|
|
725
|
-
windows = self.list_windows()
|
|
726
|
-
print(f"Active tasks: {len(windows)}")
|
|
727
|
-
self.list_tasks()
|
|
722
|
+
line = f"{health_icon} {status_text:8} {task_name:15}{port:7} {task_cfg.command}"
|
|
723
|
+
print(f"{line}{auto}{extras}")
|
|
728
724
|
|
|
729
725
|
def check_task_health(self, task_name: str) -> bool:
|
|
730
726
|
"""Check if a task is healthy"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|