tmux-manager

Sesiones tmux declarativas, recuperables y con historial completo

Una forma simple de levantar tus espacios de trabajo como código: cada ventana tmux queda declarada en JSON, el daemon la mantiene viva y todo lo que pasa en pantalla queda guardado.

Qué es

Sesiones declarativas Definís `daily`, `personal`, `remote` y sus ventanas en `~/.tmux-manager/config.json`.
Codex izquierda, Claude derecha Por default cada window abre dos panes: `codex` a la izquierda y `claude` a la derecha.
Auto-restore y grabación Si falta una sesión o ventana, la recrea. Además usa `pipe-pane` y snapshots para no perder contexto.

El manager es aditivo: crea lo que falta, reabre pipes si hace falta y no mata sesiones o ventanas sanas. Esa decisión es clave porque lo podés dejar corriendo en el server tmux default sin que te borre trabajo.

Diagramas

Arquitectura

flowchart LR cfg["~/.tmux-manager/config.json"] --> watch["tmux-manager watch
LaunchAgent"] watch --> tmux["server tmux default"] tmux --> daily["daily"] tmux --> personal["personal"] tmux --> remote["remote"] watch --> logs["pipe-pane logs
~/.tmux-manager/logs"] watch --> snaps["snapshots
~/.tmux-manager/snapshots"] watch --> daemon["watch-daemon.log"]

Flujo de cada tick

flowchart TD start["cada 15s"] --> changed{"config cambió?"} changed -->|sí| reload["hot-reload
Config.load + resolve"] changed -->|no| reconcile reload --> valid{"JSON válido?"} valid -->|sí| reconcile["reconcile aditivo
crear lo que falta"] valid -->|no| old["seguir con config anterior
loggear error una vez"] old --> reconcile reconcile --> sweep["sweep pipes
attach si falta pipe
rotación diaria"] sweep --> snap{"pasaron 30 min?"} snap -->|sí| snapshot["snapshot scrollback"] snap -->|no| done["fin del tick"] snapshot --> done

Layout actual

flowchart TB subgraph daily hoy["hoy
codex | claude"] finanzas["finanzas
codex | claude"] comms["comms
codex | claude"] end subgraph personal artisview["artisview
codex | claude"] fonzo["fonzo
codex | claude"] agro["agro
codex | claude"] end subgraph remote shell["shell
ssh | ssh"] monitor["monitor
ssh | ssh"] end

Capturas reales

Los bloques de abajo muestran salidas neutrales del CLI y de tmux. La parte importante para operar está ahí: comandos reproducibles y output consistente.

captura neutral
$ tmux-manager status --config ~/.tmux-manager/config.json
session daily: OK
session personal: OK
session remote: OK
converged: yes

Help

tmux-manager help
$ ~/.local/bin/tmux-manager help
tmux-manager 0.1.0 — declarative tmux session manager

USAGE:
    tmux-manager [global options] <command>

COMMANDS:
    up                    Create/attach sessions from config
    status                Show managed sessions status
    save                  Snapshot every managed pane's scrollback
    watch                 Convergence loop: up + pipe logs + snapshots
      --interval N          Seconds between passes (default: 15)
    logs                  List pane log files (grouped by session)
      --path <sess>:<win>   Print today's log path(s) for a window
      --prune --days N      Delete logs older than N days
    install-launchagent   Write the launchd plist (does NOT launchctl)
    help                  Show this help

Status

status
$ ~/.local/bin/tmux-manager status --config ~/.tmux-manager/config.json
session daily: OK
  window hoy: OK (2 panes)
    pane %26: node [piped]
    pane %27: claude.exe [piped]
  window finanzas: OK (2 panes)
    pane %101: node [piped]
    pane %102: claude.exe [piped]
  window comms: OK (2 panes)
    pane %103: node [piped]
    pane %104: claude.exe [piped]
session personal: OK
  window artisview: OK (2 panes)
  window fonzo: OK (2 panes)
  window agro: OK (2 panes)
session remote: OK
  window shell: OK (2 panes)
    pane %139: ssh [piped]
    pane %140: ssh [piped]
converged: yes

Logs y tmux

logs | head
$ ~/.local/bin/tmux-manager logs --config ~/.tmux-manager/config.json | head -18
daily:
  hoy-p1-2026-07-09.log  1.5 KB  2026-07-09 12:08
  hoy-p0-2026-07-09.log  2.8 KB  2026-07-09 12:08
  finanzas-p1-2026-07-09.log  20 B  2026-07-09 12:08
  comms-p1-2026-07-09.log  52 B  2026-07-09 12:08
personal:
  artisview-p0-2026-07-09.log  26.4 MB  2026-07-09 12:17
  fonzo-p1-2026-07-09.log  13.6 KB  2026-07-09 12:15
remote:
  shell-p0-2026-07-09.log  9.5 KB  2026-07-09 12:11
  shell-p1-2026-07-09.log  9.5 KB  2026-07-09 12:11
tmux list-windows -a
$ tmux list-windows -a
daily:0: hoy* (2 panes)
daily:1: finanzas (2 panes)
daily:2: comms (2 panes)
personal:0: fonzo (2 panes)
personal:1: agro- (2 panes)
personal:2: artisview* (2 panes)
remote:0: shell* (2 panes)
watch-daemon.log
$ rg 'config reloaded' ~/.tmux-manager/watch-daemon.log
[2026-07-09 00:34:47] config reloaded (3 sessions) up[... windows created: 1 (daily:reload-test) ...] sweep[attached: 2, rotated: 0]
[2026-07-09 00:35:34] config reloaded (3 sessions) up[... windows existing: 8 ...] sweep[attached: 0, rotated: 0]
[2026-07-09 00:36:51] config reloaded (3 sessions) up[... windows existing: 8 ...] sweep[attached: 0, rotated: 0]

$ tail ~/.tmux-manager/watch-daemon.log
[2026-07-09 09:06:53] up[... windows existing: 8 ...] sweep[attached: 0, rotated: 0]
[2026-07-09 09:07:08] up[... windows existing: 8 ...] sweep[attached: 0, rotated: 0]
[2026-07-09 09:07:24] up[... windows existing: 8 ...] sweep[attached: 0, rotated: 0]
ls logs/remote
$ ls -la ~/.tmux-manager/logs/remote/
-rw-r--r--  shell-p0-2026-07-09.log    9728
-rw-r--r--  shell-p1-2026-07-09.log    9728
-rw-r--r--  monitor-p0-2026-07-09.log  4096
-rw-r--r--  monitor-p1-2026-07-09.log  4096

Tutorial paso a paso

1. Instalar

Desde el repo, el script compila release, instala el binario en `~/.local/bin` y escribe el LaunchAgent. El install del binario es atómico para no romper la firma/caché de macOS.

install
$ cd ~/personal/tools/tmux-manager
$ scripts/install.sh
$ ~/.local/bin/tmux-manager help

2. Editar config

El archivo real vive en `~/.tmux-manager/config.json`. Esta es la config actual usada para las capturas:

config.json
{
  "defaults": {
    "left": "codex",
    "right": "claude",
    "cwd": "~"
  },
  "sessions": [
    {
      "name": "daily",
      "cwd": "~",
      "windows": [
        { "name": "hoy" },
        { "name": "finanzas", "cwd": "~/personal/finanzas" },
        { "name": "comms", "cwd": "~/personal" }
      ]
    },
    {
      "name": "personal",
      "cwd": "~/personal",
      "windows": [
        { "name": "artisview", "cwd": "~/personal/github-repos/artisview-prod-quantlab" },
        { "name": "fonzo", "cwd": "~/personal/github-repos/fonzo" },
        { "name": "agro", "cwd": "~/personal/github-repos/proj-agro-inteligente" }
      ]
    },
    {
      "name": "remote",
      "cwd": "~",
      "windows": [
        {
          "name": "shell",
          "left": "ssh -t mi-servidor 'cd ~/proyecto && exec $SHELL'",
          "right": "ssh -t mi-servidor 'cd ~/proyecto && exec $SHELL'"
        }
      ]
    }
  ]
}

3. Hot-reload

Ya no hace falta reiniciar el daemon para cambios normales de JSON. Guardás el archivo y en el próximo tick se recarga. Si el JSON está roto, sigue corriendo con la última config buena y lo deja en el log una sola vez para ese cambio.

reload-test
// agregado temporal en daily.windows
{
  "name": "reload-test",
  "left": "",
  "right": ""
}

$ rg 'reload-test' ~/.tmux-manager/watch-daemon.log
[2026-07-09 00:34:47] config reloaded (3 sessions) ... windows created: 1 (daily:reload-test) ...

4. Si tmux muere

En el siguiente tick, `watch` vuelve a levantar el server y recrea lo que está en config. Lo que recupera bien: sesiones, ventanas, dos panes por window, pipes de logs y snapshots futuros. Lo que no puede inventar: procesos que estaban corriendo dentro del pane si murieron con el server.

Por eso los logs y snapshots importan: el historial anterior queda en `~/.tmux-manager/logs` y `~/.tmux-manager/snapshots`.

5. Leer logs

Los logs son bytes crudos de terminal, incluyendo ANSI. Abrilos con `less -R` para conservar colores.

leer logs
$ tmux-manager logs
$ tmux-manager logs --path remote:shell
$ less -R ~/.tmux-manager/logs/remote/shell-p0-2026-07-09.log

6. Snapshot manual

`save` captura el scrollback de cada pane gestionado en el momento. Es útil antes de reiniciar, cerrar la Mac o hacer limpieza.

save
$ tmux-manager save
snapshots written: 16, pruned: 0

7. LaunchAgent

El installer escribe el plist pero no ejecuta `launchctl` por vos. Para cargarlo o reiniciarlo:

launchd
$ launchctl bootout gui/$(id -u)/com.luis.tmux-manager.watch
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.luis.tmux-manager.watch.plist

# para descargarlo
$ launchctl bootout gui/$(id -u)/com.luis.tmux-manager.watch

Troubleshooting

El binario muere con SIGKILL / exit 137

No pises el binario instalado in-place. En macOS puede quedar inválido por caché/firma. Instalá atómico:

atomic install
$ cp .build/release/tmux-manager ~/.local/bin/tmux-manager.new
$ chmod +x ~/.local/bin/tmux-manager.new
$ mv -f ~/.local/bin/tmux-manager.new ~/.local/bin/tmux-manager

`launchctl bootstrap` devuelve error 5

Suele pasar justo después de `bootout`, cuando launchd todavía no terminó de soltar el job. Esperá unos segundos y reintentá.

JSON roto

El daemon no crashea. Deja un error `config reload: ...` en `watch-daemon.log` y sigue con la última config buena. Arreglás el JSON, guardás, y en el siguiente tick recarga limpio.

Pidfile stale

Si el daemon murió y quedó `~/.tmux-manager/watch.pid`, borrá el pidfile y volvé a cargar el LaunchAgent.

pidfile
$ rm -f ~/.tmux-manager/watch.pid
$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.luis.tmux-manager.watch.plist
Regla operativa: `tmux-manager` es aditivo. Si sacás una window del JSON, deja de gestionarla, pero no la mata. Si después la matás vos, no reaparece.