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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskmux
3
- Version: 0.2.6
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 (graceful C-c)
175
- taskmux stop <task> # Stop a single task (graceful C-c)
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 (graceful). Window stays alive so you can see exit output.
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 (graceful C-c)
140
- taskmux stop <task> # Stop a single task (graceful C-c)
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 (graceful). Window stays alive so you can see exit output.
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskmux"
3
- version = "0.2.6"
3
+ version = "0.2.7"
4
4
  description = "Modern tmux-based task manager for LLM development tools"
5
5
  readme = "README.md"
6
6
  license-file = "LICENSE"
@@ -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
- auto = "" if task.auto_start else " (manual)"
45
- lines.append(f"- **{name}**: `{task.command}`{auto}")
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
- task: str | None = typer.Argument(None, help="Task name (omit for all)"),
58
+ tasks: list[str] = typer.Argument(None, help="Task names (omit for all)"), # noqa: B008
66
59
  ):
67
- """Start all tasks or a specific task."""
60
+ """Start tasks (all if none specified)."""
68
61
  cli = TaskmuxCLI()
69
- if task:
70
- cli.tmux.start_task(task)
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
- task: str | None = typer.Argument(None, help="Task name (omit for all)"),
71
+ tasks: list[str] = typer.Argument(None, help="Task names (omit for all)"), # noqa: B008
78
72
  ):
79
- """Stop all tasks or a specific task (graceful C-c)."""
73
+ """Stop tasks (all if none specified)."""
80
74
  cli = TaskmuxCLI()
81
- if task:
82
- cli.tmux.stop_task(task)
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
- task: str | None = typer.Argument(None, help="Task name (omit for all)"),
84
+ tasks: list[str] = typer.Argument(None, help="Task names (omit for all)"), # noqa: B008
90
85
  ):
91
- """Restart all tasks or a specific task."""
86
+ """Restart tasks (all if none specified)."""
92
87
  cli = TaskmuxCLI()
93
- if task:
94
- cli.tmux.restart_task(task)
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
- @app.command()
164
- def status():
165
- """Show session status."""
159
+ def _status():
160
+ """Show session and task status."""
166
161
  cli = TaskmuxCLI()
167
- cli.tmux.show_status()
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
- print(f"Session: {self.config.name}")
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
- print(f"{health_icon} {status_text:8} {task_name:15} {task_cfg.command}{auto}{extras}")
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