@danielishi/lmux 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/lmux +225 -0
- package/bin/lmux-grid +45 -0
- package/bin/lmux-read +9 -0
- package/bin/lmux-send +10 -0
- package/bin/lmux-send-key +9 -0
- package/commands/lmux-team.md +123 -0
- package/commands/lmux.md +183 -0
- package/package.json +28 -0
- package/skills/using-lmux/SKILL.md +162 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Bratschke / SpockyMagicAI
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# lmux
|
|
2
|
+
|
|
3
|
+
**lmux — Linux terminal multiplexer for Claude Code agent orchestration. The Linux peer of cmux (macOS) and wmux (Windows).**
|
|
4
|
+
|
|
5
|
+
lmux is a thin CLI wrapper around `tmux` that exposes a stable, scriptable interface for orchestrating multiple Claude Code agents in panes, splits, and workspaces. It mirrors the cmux API one-to-one so the same orchestration code, skills, and agent definitions work unchanged across all three platforms.
|
|
6
|
+
|
|
7
|
+
## Platform ecosystem
|
|
8
|
+
|
|
9
|
+
| Platform | Tool | Backend | Install |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| Windows | [wmux](https://github.com/openwong2kim/wmux) | Electron + ConPTY | Installer |
|
|
12
|
+
| macOS | [cmux](https://github.com/manaflow-ai/cmux) | Swift / AppKit | Homebrew |
|
|
13
|
+
| **Linux** | **lmux** | **tmux wrapper** | **curl \| bash** |
|
|
14
|
+
|
|
15
|
+
All three expose the same CLI surface (`<x>mux send`, `read-screen`, `new-split`, `tree`, `identify`, ...), so an agent skill written against one runs against all of them.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Linux (any distribution with bash)
|
|
20
|
+
- `tmux` >= 3.0
|
|
21
|
+
- `bash` >= 4.0
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Preferred (npm):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g lmux
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Alternative (curl | bash):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -sL https://raw.githubusercontent.com/DanielIshi/lmux/master/install.sh | bash
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The installer symlinks `lmux*` into `/usr/local/bin/` and verifies the install with `lmux identify`. The npm install registers the `lmux*` binaries on your `PATH` via npm's global bin directory.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
tmux new-session -d -s agents # start a tmux session
|
|
43
|
+
lmux new-split right # → surface:2
|
|
44
|
+
lmux send --surface surface:1 "claude\n" # launch claude in left pane
|
|
45
|
+
lmux send --surface surface:2 "claude\n" # launch claude in right pane
|
|
46
|
+
lmux read-screen --surface surface:1 # read left pane output
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Command reference
|
|
50
|
+
|
|
51
|
+
| Command | Flags | Description |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `lmux send` | `--surface surface:N "text"` | Send text to pane (append `\n` for Enter) |
|
|
54
|
+
| `lmux send-key` | `--surface surface:N <key>` | Send key: `return`, `ctrl+c`, `ctrl+d`, etc. |
|
|
55
|
+
| `lmux read-screen` | `--surface surface:N [--scrollback] [--lines N]` | Read pane output |
|
|
56
|
+
| `lmux new-split` | `right\|down` | Split pane, returns new `surface:N` |
|
|
57
|
+
| `lmux close-surface` | `--surface surface:N` | Kill pane |
|
|
58
|
+
| `lmux tree` | `--all --json` | JSON hierarchy of all panes |
|
|
59
|
+
| `lmux identify` | — | Verify lmux (tmux) environment |
|
|
60
|
+
| `lmux list-workspaces` | — | List workspaces (tmux windows) |
|
|
61
|
+
| `lmux notify` | `--title T --body B` | Desktop notification |
|
|
62
|
+
| `lmux-send` | `--surface surface:N "text"` | Wrapper: auto-resolves surface refs |
|
|
63
|
+
| `lmux-read` | `--surface surface:N [--scrollback]` | Wrapper: auto-resolves surface refs |
|
|
64
|
+
| `lmux-send-key` | `--surface surface:N <key>` | Wrapper: auto-resolves surface refs |
|
|
65
|
+
| `lmux-grid` | `<rows> <cols>` | Build a rows×cols grid of panes; returns JSON array of new `surface:N` refs |
|
|
66
|
+
|
|
67
|
+
See [commands/lmux.md](commands/lmux.md) for full per-command documentation and tmux equivalents.
|
|
68
|
+
|
|
69
|
+
## Wrapper scripts
|
|
70
|
+
|
|
71
|
+
`lmux-send`, `lmux-read`, `lmux-send-key`, and `lmux-grid` are thin convenience wrappers around the core commands. They:
|
|
72
|
+
|
|
73
|
+
- Auto-resolve surface references against the active workspace (no need to spell out the full tmux target).
|
|
74
|
+
- Apply newline normalization automatically.
|
|
75
|
+
- Are the recommended entrypoints for agent skills and orchestration loops — the bare `lmux <verb>` form is the lower-level primitive.
|
|
76
|
+
|
|
77
|
+
`lmux-grid <rows> <cols>` builds a rows×cols pane grid in the current workspace and prints a JSON array of the new `surface:N` references — convenient for spawning a swarm of agents in one call.
|
|
78
|
+
|
|
79
|
+
## Newline rules (critical)
|
|
80
|
+
|
|
81
|
+
Same semantics as cmux — get this wrong and your agent will type its prompt without ever pressing Enter:
|
|
82
|
+
|
|
83
|
+
- **`lmux send "text"`** — sends literal text. **No Enter is pressed.**
|
|
84
|
+
- **`lmux send "text\n"`** — sends text *and* Enter. Use this to submit a prompt.
|
|
85
|
+
- **`lmux send-key return`** — presses Enter on its own (e.g. to dismiss a prompt).
|
|
86
|
+
- Multi-line input: embed `\n` between lines; the final `\n` submits.
|
|
87
|
+
- **Never** rely on a trailing space or shell behavior to submit — be explicit.
|
|
88
|
+
|
|
89
|
+
## Claude Code integration
|
|
90
|
+
|
|
91
|
+
lmux is designed to be driven by Claude Code agents. See the `using-lmux` skill at [`skills/using-lmux/SKILL.md`](skills/using-lmux/SKILL.md) for the canonical agent-side usage patterns, including the orchestration loop, polling cadence, and cleanup contract.
|
|
92
|
+
|
|
93
|
+
For multi-agent setups (lead + worker panes, cross-server SSH panes), see [commands/lmux-team.md](commands/lmux-team.md).
|
|
94
|
+
|
|
95
|
+
## Uninstall
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm rm -g lmux
|
|
99
|
+
# or, if installed via curl|bash:
|
|
100
|
+
sudo rm /usr/local/bin/lmux*
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
- **`lmux: command not found`** — the `lmux*` binaries are not on your `PATH`. For npm installs, check that npm's global bin directory (`npm config get prefix`/`bin`) is on `PATH`. For curl|bash installs, verify the symlinks landed in `/usr/local/bin/` and that this directory is on `PATH`.
|
|
106
|
+
- **`lmux: not inside a tmux session (TMUX env var unset)`** — every lmux command (except `identify` reporting the error) must run inside a tmux session. Start one first: `tmux new -s agents`, then re-run your command from inside that session.
|
|
107
|
+
- **`tmux >= 3.0 required`** — check your tmux version with `tmux -V`. Upgrade via your package manager (`apt install tmux`, `dnf install tmux`, `brew install tmux`). Distributions on older LTS lines (e.g. Ubuntu 18.04) ship 2.x and will not work — install from a backports repo or build from source.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/bin/lmux
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lmux — Linux tmux wrapper with a CLI surface compatible with cmux/wmux.
|
|
3
|
+
# Surface refs are formatted "surface:N" or "session:N" where N is a tmux
|
|
4
|
+
# pane index. The wrapper extracts N and calls tmux with `-t N`.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
_die() { echo "lmux: $*" >&2; exit 2; }
|
|
9
|
+
|
|
10
|
+
# Extract the trailing pane index from a surface ref like "surface:3" or "main:3".
|
|
11
|
+
_surface_index() {
|
|
12
|
+
local ref="$1"
|
|
13
|
+
case "$ref" in
|
|
14
|
+
*:*) echo "${ref##*:}" ;;
|
|
15
|
+
*) _die "invalid surface ref: $ref (expected surface:N)" ;;
|
|
16
|
+
esac
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Parse a flag pair "--name value" out of an arg list, echoing the value.
|
|
20
|
+
# Usage: val=$(_get_flag --surface "$@")
|
|
21
|
+
_get_flag() {
|
|
22
|
+
local name="$1"; shift
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
if [ "$1" = "$name" ]; then
|
|
25
|
+
echo "$2"
|
|
26
|
+
return 0
|
|
27
|
+
fi
|
|
28
|
+
shift
|
|
29
|
+
done
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
cmd_send() {
|
|
34
|
+
local surface text
|
|
35
|
+
surface=$(_get_flag --surface "$@") || _die "send: missing --surface"
|
|
36
|
+
# Last positional arg is the text payload.
|
|
37
|
+
text=""
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--surface) shift 2 ;;
|
|
41
|
+
*) text="$1"; shift ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
[ -z "$text" ] && _die "send: missing text"
|
|
45
|
+
local idx
|
|
46
|
+
idx=$(_surface_index "$surface")
|
|
47
|
+
|
|
48
|
+
if [[ "$text" == *'\n' ]]; then
|
|
49
|
+
local body="${text%\\n}"
|
|
50
|
+
tmux send-keys -t "$idx" "$body" Enter
|
|
51
|
+
else
|
|
52
|
+
tmux send-keys -t "$idx" "$text"
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cmd_send_key() {
|
|
57
|
+
local surface key
|
|
58
|
+
surface=$(_get_flag --surface "$@") || _die "send-key: missing --surface"
|
|
59
|
+
key=""
|
|
60
|
+
while [ $# -gt 0 ]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--surface) shift 2 ;;
|
|
63
|
+
*) key="$1"; shift ;;
|
|
64
|
+
esac
|
|
65
|
+
done
|
|
66
|
+
[ -z "$key" ] && _die "send-key: missing key name"
|
|
67
|
+
local idx
|
|
68
|
+
idx=$(_surface_index "$surface")
|
|
69
|
+
|
|
70
|
+
local mapped
|
|
71
|
+
case "$key" in
|
|
72
|
+
return|enter) mapped="Enter" ;;
|
|
73
|
+
escape|esc) mapped="Escape" ;;
|
|
74
|
+
tab) mapped="Tab" ;;
|
|
75
|
+
space) mapped="Space" ;;
|
|
76
|
+
backspace|bs) mapped="BSpace" ;;
|
|
77
|
+
up) mapped="Up" ;;
|
|
78
|
+
down) mapped="Down" ;;
|
|
79
|
+
left) mapped="Left" ;;
|
|
80
|
+
right) mapped="Right" ;;
|
|
81
|
+
ctrl+*) mapped="C-${key#ctrl+}" ;;
|
|
82
|
+
alt+*) mapped="M-${key#alt+}" ;;
|
|
83
|
+
*) _die "send-key: unknown key '$key'" ;;
|
|
84
|
+
esac
|
|
85
|
+
|
|
86
|
+
tmux send-keys -t "$idx" "$mapped"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cmd_read_screen() {
|
|
90
|
+
local surface scrollback=0 lines=""
|
|
91
|
+
surface=$(_get_flag --surface "$@") || _die "read-screen: missing --surface"
|
|
92
|
+
while [ $# -gt 0 ]; do
|
|
93
|
+
case "$1" in
|
|
94
|
+
--surface) shift 2 ;;
|
|
95
|
+
--scrollback) scrollback=1; shift ;;
|
|
96
|
+
--lines) lines="$2"; shift 2 ;;
|
|
97
|
+
*) shift ;;
|
|
98
|
+
esac
|
|
99
|
+
done
|
|
100
|
+
local idx
|
|
101
|
+
idx=$(_surface_index "$surface")
|
|
102
|
+
|
|
103
|
+
if [ -n "$lines" ]; then
|
|
104
|
+
tmux capture-pane -t "$idx" -p -S "-${lines}"
|
|
105
|
+
elif [ "$scrollback" = "1" ]; then
|
|
106
|
+
tmux capture-pane -t "$idx" -p -S -
|
|
107
|
+
else
|
|
108
|
+
tmux capture-pane -t "$idx" -p
|
|
109
|
+
fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cmd_new_split() {
|
|
113
|
+
local dir="${1:-}"
|
|
114
|
+
local flag
|
|
115
|
+
case "$dir" in
|
|
116
|
+
right) flag="-h" ;;
|
|
117
|
+
down) flag="-v" ;;
|
|
118
|
+
*) _die "new-split: direction must be 'right' or 'down'" ;;
|
|
119
|
+
esac
|
|
120
|
+
tmux split-window "$flag" -P -F "surface:#{pane_index}"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cmd_close_surface() {
|
|
124
|
+
local surface
|
|
125
|
+
surface=$(_get_flag --surface "$@") || _die "close-surface: missing --surface"
|
|
126
|
+
local idx
|
|
127
|
+
idx=$(_surface_index "$surface")
|
|
128
|
+
tmux kill-pane -t "$idx"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cmd_tree() {
|
|
132
|
+
# Flags ignored beyond --all --json; the only supported mode.
|
|
133
|
+
local fmt="#{session_name}:#{window_index}:#{pane_index}:#{pane_title}"
|
|
134
|
+
if command -v jq >/dev/null 2>&1; then
|
|
135
|
+
tmux list-panes -a -F "$fmt" \
|
|
136
|
+
| jq -R 'select(length > 0) | split(":") | {session: .[0], window: (.[1]|tonumber), pane: (.[2]|tonumber), surface: ("surface:" + .[2]), title: (.[3] // "")}' \
|
|
137
|
+
| jq -s .
|
|
138
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
139
|
+
tmux list-panes -a -F "$fmt" \
|
|
140
|
+
| python3 -c "import sys,json; rows=[l.split(':',3) for l in sys.stdin.read().splitlines() if l]; print(json.dumps([{'session':r[0],'window':int(r[1]),'pane':int(r[2]),'surface':f'surface:{r[2]}','title':r[3] if len(r)>3 else ''} for r in rows]))"
|
|
141
|
+
else
|
|
142
|
+
echo "lmux tree: warning — neither jq nor python3 found, falling back to naive JSON escape (titles with special chars may break)" >&2
|
|
143
|
+
local raw
|
|
144
|
+
raw=$(tmux list-panes -a -F "$fmt")
|
|
145
|
+
printf '['
|
|
146
|
+
local first=1
|
|
147
|
+
while IFS= read -r line; do
|
|
148
|
+
[ -z "$line" ] && continue
|
|
149
|
+
IFS=':' read -r session window pane title <<<"$line"
|
|
150
|
+
[ "$first" = "1" ] || printf ','
|
|
151
|
+
first=0
|
|
152
|
+
local esc_title
|
|
153
|
+
esc_title=${title//\\/\\\\}
|
|
154
|
+
esc_title=${esc_title//\"/\\\"}
|
|
155
|
+
printf '{"session":"%s","window":%s,"pane":%s,"surface":"surface:%s","title":"%s"}' \
|
|
156
|
+
"$session" "$window" "$pane" "$pane" "$esc_title"
|
|
157
|
+
done <<<"$raw"
|
|
158
|
+
printf ']\n'
|
|
159
|
+
fi
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cmd_identify() {
|
|
163
|
+
if [ -n "${TMUX:-}" ]; then
|
|
164
|
+
echo "tmux: ${TMUX}"
|
|
165
|
+
exit 0
|
|
166
|
+
else
|
|
167
|
+
echo "lmux: not inside a tmux session (TMUX env var unset)" >&2
|
|
168
|
+
exit 1
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
cmd_list_workspaces() {
|
|
173
|
+
tmux list-windows -F "workspace:#{window_index} #{window_name}"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
cmd_notify() {
|
|
177
|
+
local title="" body=""
|
|
178
|
+
while [ $# -gt 0 ]; do
|
|
179
|
+
case "$1" in
|
|
180
|
+
--title) title="$2"; shift 2 ;;
|
|
181
|
+
--body) body="$2"; shift 2 ;;
|
|
182
|
+
*) shift ;;
|
|
183
|
+
esac
|
|
184
|
+
done
|
|
185
|
+
if command -v notify-send >/dev/null 2>&1; then
|
|
186
|
+
notify-send "$title" "$body"
|
|
187
|
+
else
|
|
188
|
+
echo "[lmux notify] $title: $body"
|
|
189
|
+
fi
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main() {
|
|
193
|
+
local sub="${1:-}"
|
|
194
|
+
shift || true
|
|
195
|
+
case "$sub" in
|
|
196
|
+
send) cmd_send "$@" ;;
|
|
197
|
+
send-key) cmd_send_key "$@" ;;
|
|
198
|
+
read-screen) cmd_read_screen "$@" ;;
|
|
199
|
+
new-split) cmd_new_split "$@" ;;
|
|
200
|
+
close-surface) cmd_close_surface "$@" ;;
|
|
201
|
+
tree) cmd_tree "$@" ;;
|
|
202
|
+
identify) cmd_identify "$@" ;;
|
|
203
|
+
list-workspaces) cmd_list_workspaces "$@" ;;
|
|
204
|
+
notify) cmd_notify "$@" ;;
|
|
205
|
+
""|-h|--help)
|
|
206
|
+
cat <<EOF
|
|
207
|
+
lmux — Linux tmux wrapper (cmux/wmux-compatible CLI)
|
|
208
|
+
|
|
209
|
+
Commands:
|
|
210
|
+
send --surface S:N "text\\n"
|
|
211
|
+
send-key --surface S:N <return|escape|tab|ctrl+c|...>
|
|
212
|
+
read-screen --surface S:N [--scrollback | --lines N]
|
|
213
|
+
new-split <right|down>
|
|
214
|
+
close-surface --surface S:N
|
|
215
|
+
tree --all --json
|
|
216
|
+
identify
|
|
217
|
+
list-workspaces
|
|
218
|
+
notify --title T --body B
|
|
219
|
+
EOF
|
|
220
|
+
;;
|
|
221
|
+
*) _die "unknown subcommand: $sub" ;;
|
|
222
|
+
esac
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
main "$@"
|
package/bin/lmux-grid
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wrapper: lmux-grid <rows> <cols>
|
|
3
|
+
# Creates an N-row x M-col grid of panes via repeated splits and emits a JSON
|
|
4
|
+
# array of the resulting surface refs (the new panes only, current pane first).
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
if [ $# -ne 2 ]; then
|
|
8
|
+
echo "usage: lmux-grid <rows> <cols>" >&2
|
|
9
|
+
exit 2
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
rows="$1"; cols="$2"
|
|
13
|
+
total=$((rows * cols))
|
|
14
|
+
if [ "$total" -lt 1 ]; then
|
|
15
|
+
echo "[]"
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
surfaces=()
|
|
20
|
+
# We need total-1 new splits to reach total panes from the current one.
|
|
21
|
+
# Strategy: alternate vertical (down) and horizontal (right) splits.
|
|
22
|
+
n=$((total - 1))
|
|
23
|
+
i=0
|
|
24
|
+
while [ "$i" -lt "$n" ]; do
|
|
25
|
+
if [ $((i % 2)) -eq 0 ]; then
|
|
26
|
+
s=$(lmux new-split right)
|
|
27
|
+
else
|
|
28
|
+
s=$(lmux new-split down)
|
|
29
|
+
fi
|
|
30
|
+
surfaces+=("$s")
|
|
31
|
+
i=$((i + 1))
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
# Apply tiled layout for evenness (best-effort; mock tmux ignores).
|
|
35
|
+
tmux select-layout tiled >/dev/null 2>&1 || true
|
|
36
|
+
|
|
37
|
+
# Emit JSON.
|
|
38
|
+
printf '['
|
|
39
|
+
first=1
|
|
40
|
+
for s in "${surfaces[@]}"; do
|
|
41
|
+
[ "$first" = "1" ] || printf ','
|
|
42
|
+
first=0
|
|
43
|
+
printf '"%s"' "$s"
|
|
44
|
+
done
|
|
45
|
+
printf ']\n'
|
package/bin/lmux-read
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wrapper: lmux-read <surface:N> [--scrollback | --lines N]
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
if [ $# -lt 1 ]; then
|
|
5
|
+
echo "usage: lmux-read <surface:N> [--scrollback | --lines N]" >&2
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
surface="$1"; shift
|
|
9
|
+
exec lmux read-screen --surface "$surface" "$@"
|
package/bin/lmux-send
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wrapper: lmux-send <surface:N> <text>
|
|
3
|
+
# Mirrors cmux-send. Just forwards to `lmux send --surface S T`.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
if [ $# -lt 2 ]; then
|
|
6
|
+
echo "usage: lmux-send <surface:N> <text>" >&2
|
|
7
|
+
exit 2
|
|
8
|
+
fi
|
|
9
|
+
surface="$1"; shift
|
|
10
|
+
exec lmux send --surface "$surface" "$*"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# `lmux-team` — Multi-agent orchestration with lmux
|
|
2
|
+
|
|
3
|
+
This guide shows how to drive multiple Claude Code agents from a single lead process using lmux. It assumes you've read [`lmux.md`](lmux.md) and understand surfaces, newline rules, and the wrapper scripts.
|
|
4
|
+
|
|
5
|
+
## Creating a 2-agent session
|
|
6
|
+
|
|
7
|
+
The canonical "lead + worker" layout: a left pane running the orchestrator, a right pane running a worker agent.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Start a dedicated tmux session (detached, so we can drive it from outside)
|
|
11
|
+
tmux new-session -d -s agents -x 200 -y 50
|
|
12
|
+
|
|
13
|
+
# 2. Confirm the root pane
|
|
14
|
+
lmux identify
|
|
15
|
+
# → session=agents, window=0, surface=surface:1
|
|
16
|
+
|
|
17
|
+
# 3. Split right to create the worker pane
|
|
18
|
+
WORKER=$(lmux new-split right)
|
|
19
|
+
# → surface:2
|
|
20
|
+
|
|
21
|
+
# 4. Launch claude in each pane
|
|
22
|
+
lmux send --surface surface:1 "claude\n"
|
|
23
|
+
lmux send --surface "$WORKER" "claude\n"
|
|
24
|
+
|
|
25
|
+
# 5. Wait for both to settle, then dispatch
|
|
26
|
+
sleep 3
|
|
27
|
+
lmux send --surface "$WORKER" "Read README.md and summarize it in 3 bullets.\n"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The lead pane is now free to receive its own prompts, monitor the worker, or spawn more splits.
|
|
31
|
+
|
|
32
|
+
## The orchestration loop
|
|
33
|
+
|
|
34
|
+
This is the pattern you'll use 90% of the time: dispatch a task, poll for completion, collect the result, clean up.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dispatch() {
|
|
38
|
+
local surface="$1" prompt="$2"
|
|
39
|
+
lmux send --surface "$surface" "$prompt\n"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
wait_for_idle() {
|
|
43
|
+
# Poll until the pane stops changing for `stable_for` seconds,
|
|
44
|
+
# or until `timeout` seconds elapse.
|
|
45
|
+
local surface="$1" timeout="${2:-300}" stable_for="${3:-5}"
|
|
46
|
+
local last="" now="" stable_since=$SECONDS deadline=$((SECONDS + timeout))
|
|
47
|
+
|
|
48
|
+
while [ $SECONDS -lt $deadline ]; do
|
|
49
|
+
now=$(lmux read-screen --surface "$surface" --lines 40)
|
|
50
|
+
if [ "$now" = "$last" ]; then
|
|
51
|
+
if [ $((SECONDS - stable_since)) -ge "$stable_for" ]; then
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
else
|
|
55
|
+
last="$now"
|
|
56
|
+
stable_since=$SECONDS
|
|
57
|
+
fi
|
|
58
|
+
sleep 1
|
|
59
|
+
done
|
|
60
|
+
return 1 # timed out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
collect() {
|
|
64
|
+
local surface="$1"
|
|
65
|
+
lmux read-screen --surface "$surface" --scrollback
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
cleanup() {
|
|
69
|
+
local surface="$1"
|
|
70
|
+
lmux send-key --surface "$surface" ctrl+c # interrupt
|
|
71
|
+
sleep 1
|
|
72
|
+
lmux send --surface "$surface" "/exit\n" # graceful claude exit
|
|
73
|
+
sleep 1
|
|
74
|
+
lmux close-surface --surface "$surface"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Usage
|
|
78
|
+
WORKER=$(lmux new-split right)
|
|
79
|
+
lmux send --surface "$WORKER" "claude\n"
|
|
80
|
+
sleep 3
|
|
81
|
+
dispatch "$WORKER" "Summarize README.md in 3 bullets."
|
|
82
|
+
wait_for_idle "$WORKER" 180 5 || echo "Worker timed out"
|
|
83
|
+
RESULT=$(collect "$WORKER")
|
|
84
|
+
cleanup "$WORKER"
|
|
85
|
+
echo "$RESULT"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Tuning the loop
|
|
89
|
+
|
|
90
|
+
- **`stable_for`** — raise it for agents that pause mid-thought (3–10 s typical).
|
|
91
|
+
- **`timeout`** — set generously; a Claude task that exceeds it is almost always stuck, not slow.
|
|
92
|
+
- **Polling cadence** — 1 s is the sweet spot. Faster wastes CPU on `capture-pane`; slower delays the next dispatch.
|
|
93
|
+
- **Idempotency** — always design tasks so re-dispatching is safe; transient network blips happen.
|
|
94
|
+
|
|
95
|
+
## Cross-server pattern (SSH pane + lmux)
|
|
96
|
+
|
|
97
|
+
To drive an agent on another machine, dedicate a pane to an SSH session and treat it like any other surface.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
REMOTE=$(lmux new-split down)
|
|
101
|
+
lmux send --surface "$REMOTE" "ssh user@host\n"
|
|
102
|
+
sleep 2 # wait for login
|
|
103
|
+
lmux send --surface "$REMOTE" "cd /srv/app && claude\n"
|
|
104
|
+
sleep 3
|
|
105
|
+
lmux send --surface "$REMOTE" "Show me the last 20 log lines.\n"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Caveats:
|
|
109
|
+
|
|
110
|
+
- **Authentication** — use key-based SSH; don't expect the orchestrator to type passwords.
|
|
111
|
+
- **Liveness** — keepalives (`ServerAliveInterval 30` in `~/.ssh/config`) prevent silent disconnects from breaking the polling loop.
|
|
112
|
+
- **Detection** — `lmux read-screen` shows whatever the remote terminal renders. If the remote shell prompt looks identical to the local one, distinguish them with a unique `PS1` over SSH.
|
|
113
|
+
- **Cleanup** — exit the remote shell *before* closing the surface to avoid orphaned remote processes.
|
|
114
|
+
|
|
115
|
+
## Integration with the agent-mux skill
|
|
116
|
+
|
|
117
|
+
The `agent-mux` skill (shared across wmux/cmux/lmux) wraps this entire pattern in a stable, declarative interface:
|
|
118
|
+
|
|
119
|
+
- It picks the right backend (`wmux` / `cmux` / `lmux`) based on the host OS — your skill code doesn't change.
|
|
120
|
+
- It provides ready-made helpers for dispatch / wait / collect / cleanup with sane defaults.
|
|
121
|
+
- It coordinates multiple worker surfaces (fan-out / fan-in) and aggregates results.
|
|
122
|
+
|
|
123
|
+
See the `using-lmux` skill (`skills/using-lmux/SKILL.md`) for the Linux-specific notes, and the `agent-mux` skill for the cross-platform orchestration API. When writing a new orchestration skill, prefer `agent-mux` over driving `lmux` directly — it's what guarantees your skill works unchanged on macOS and Windows too.
|
package/commands/lmux.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# `lmux` command reference
|
|
2
|
+
|
|
3
|
+
`lmux` is a tmux-backed CLI for orchestrating Claude Code agents on Linux. It is API-compatible with `cmux` (macOS) and `wmux` (Windows) — the same flags, the same surface model, the same newline rules.
|
|
4
|
+
|
|
5
|
+
## Concepts
|
|
6
|
+
|
|
7
|
+
### Surface references
|
|
8
|
+
|
|
9
|
+
Every pane is addressed as `surface:N`, where `N` is a stable integer assigned by lmux when the pane is created. Surfaces are scoped to the active tmux session and survive splits, layout changes, and zoom toggles.
|
|
10
|
+
|
|
11
|
+
- The first/root pane of a session is `surface:1`.
|
|
12
|
+
- `lmux new-split` returns the new surface ID on stdout.
|
|
13
|
+
- `lmux tree --json` lists all surfaces with their parents, sizes, and TTY paths.
|
|
14
|
+
|
|
15
|
+
### Control key sequences
|
|
16
|
+
|
|
17
|
+
`lmux send-key` accepts symbolic names. The most common:
|
|
18
|
+
|
|
19
|
+
| Name | Effect |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `return` / `enter` | Press Enter |
|
|
22
|
+
| `tab` | Press Tab |
|
|
23
|
+
| `escape` / `esc` | Press Escape |
|
|
24
|
+
| `space` | Press Space |
|
|
25
|
+
| `backspace` | Press Backspace |
|
|
26
|
+
| `up` / `down` / `left` / `right` | Arrow keys |
|
|
27
|
+
| `ctrl+c` | Interrupt (SIGINT) |
|
|
28
|
+
| `ctrl+d` | EOF / exit |
|
|
29
|
+
| `ctrl+l` | Clear screen |
|
|
30
|
+
| `ctrl+z` | Suspend |
|
|
31
|
+
| `pageup` / `pagedown` | Page navigation |
|
|
32
|
+
|
|
33
|
+
Compound modifiers use `+`: `ctrl+a`, `alt+f`, `shift+tab`.
|
|
34
|
+
|
|
35
|
+
### Newline handling
|
|
36
|
+
|
|
37
|
+
This is the single most common source of "agent hung" bugs. Internalize it.
|
|
38
|
+
|
|
39
|
+
- `lmux send "hello"` — types `hello`. **No Enter.**
|
|
40
|
+
- `lmux send "hello\n"` — types `hello` *and* presses Enter.
|
|
41
|
+
- `lmux send-key return` — presses Enter without typing anything.
|
|
42
|
+
- `lmux send "line1\nline2\n"` — types two lines and submits.
|
|
43
|
+
|
|
44
|
+
The wrappers (`lmux-send`, `lmux-read`, `lmux-send-key`) follow the same rules.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
### `lmux send`
|
|
51
|
+
|
|
52
|
+
Send literal text to a pane.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
lmux send --surface surface:N "text"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **Flags:**
|
|
59
|
+
- `--surface surface:N` (required) — target pane
|
|
60
|
+
- `"text"` (positional) — payload; `\n` is interpreted as Enter
|
|
61
|
+
- **Example:**
|
|
62
|
+
```
|
|
63
|
+
lmux send --surface surface:2 "echo hello\n"
|
|
64
|
+
```
|
|
65
|
+
- **tmux equivalent:** `tmux send-keys -t <target> "echo hello" Enter`
|
|
66
|
+
|
|
67
|
+
### `lmux send-key`
|
|
68
|
+
|
|
69
|
+
Press a named key or key combo.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
lmux send-key --surface surface:N <key>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Flags:**
|
|
76
|
+
- `--surface surface:N` (required)
|
|
77
|
+
- `<key>` (positional) — see Control key sequences above
|
|
78
|
+
- **Example:**
|
|
79
|
+
```
|
|
80
|
+
lmux send-key --surface surface:1 ctrl+c
|
|
81
|
+
```
|
|
82
|
+
- **tmux equivalent:** `tmux send-keys -t <target> C-c`
|
|
83
|
+
|
|
84
|
+
### `lmux read-screen`
|
|
85
|
+
|
|
86
|
+
Read the current visible pane buffer.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
lmux read-screen --surface surface:N [--scrollback] [--lines N]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **Flags:**
|
|
93
|
+
- `--surface surface:N` (required)
|
|
94
|
+
- `--scrollback` — include history above the visible region
|
|
95
|
+
- `--lines N` — limit output to the last N lines
|
|
96
|
+
- **Example:**
|
|
97
|
+
```
|
|
98
|
+
lmux read-screen --surface surface:2 --lines 50
|
|
99
|
+
```
|
|
100
|
+
- **tmux equivalent:** `tmux capture-pane -p -t <target> [-S -]`
|
|
101
|
+
|
|
102
|
+
### `lmux new-split`
|
|
103
|
+
|
|
104
|
+
Split the current (or specified) pane and return the new surface ID.
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
lmux new-split right|down [--surface surface:N]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **Args:**
|
|
111
|
+
- `right` — vertical split (new pane on the right)
|
|
112
|
+
- `down` — horizontal split (new pane below)
|
|
113
|
+
- **Output:** the new `surface:N` on stdout
|
|
114
|
+
- **Example:**
|
|
115
|
+
```
|
|
116
|
+
NEW=$(lmux new-split right)
|
|
117
|
+
lmux send --surface "$NEW" "claude\n"
|
|
118
|
+
```
|
|
119
|
+
- **tmux equivalent:** `tmux split-window -h` / `-v`
|
|
120
|
+
|
|
121
|
+
### `lmux close-surface`
|
|
122
|
+
|
|
123
|
+
Kill a pane.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
lmux close-surface --surface surface:N
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- **tmux equivalent:** `tmux kill-pane -t <target>`
|
|
130
|
+
|
|
131
|
+
### `lmux tree`
|
|
132
|
+
|
|
133
|
+
Dump the pane hierarchy.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
lmux tree [--all] [--json]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- **Flags:**
|
|
140
|
+
- `--all` — include every workspace (tmux window), not just the active one
|
|
141
|
+
- `--json` — emit machine-readable JSON
|
|
142
|
+
- **Example:**
|
|
143
|
+
```
|
|
144
|
+
lmux tree --all --json | jq '.workspaces[].surfaces[].id'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `lmux identify`
|
|
148
|
+
|
|
149
|
+
Verify lmux is functional in the current environment. Prints the lmux version, tmux version, active session/window/pane, and exits non-zero if anything is off.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
lmux identify
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `lmux list-workspaces`
|
|
156
|
+
|
|
157
|
+
List workspaces (tmux windows) in the active session.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
lmux list-workspaces
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **tmux equivalent:** `tmux list-windows`
|
|
164
|
+
|
|
165
|
+
### `lmux notify`
|
|
166
|
+
|
|
167
|
+
Fire a desktop notification (best-effort; uses `notify-send` if available).
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
lmux notify --title "Build done" --body "All tests passed"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Wrapper scripts
|
|
176
|
+
|
|
177
|
+
`lmux-send`, `lmux-read`, `lmux-send-key` are the recommended entrypoints for agents. They mirror the bare verbs but:
|
|
178
|
+
|
|
179
|
+
- Auto-resolve `surface:N` against the active workspace (no need to pass session/window).
|
|
180
|
+
- Normalize newlines.
|
|
181
|
+
- Emit the same exit codes as the underlying commands.
|
|
182
|
+
|
|
183
|
+
Use the wrappers in skills and orchestration loops; use the bare verbs only when you need precise control over a non-active workspace.
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@danielishi/lmux",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Linux terminal multiplexer companion for Claude Code agents — drop-in API equivalent of cmux (macOS) and wmux (Windows)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"lmux": "./bin/lmux",
|
|
7
|
+
"lmux-send": "./bin/lmux-send",
|
|
8
|
+
"lmux-read": "./bin/lmux-read",
|
|
9
|
+
"lmux-send-key": "./bin/lmux-send-key",
|
|
10
|
+
"lmux-grid": "./bin/lmux-grid"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('tmux -V', { stdio: 'ignore' }); } catch { console.warn('[lmux] WARNING: tmux not found on PATH. Install it (apt/dnf/brew install tmux) for lmux to work.'); }\"",
|
|
14
|
+
"test": "echo 'Tests use bats — run: bats tests/'"
|
|
15
|
+
},
|
|
16
|
+
"files": ["bin/", "README.md", "LICENSE", "commands/", "skills/"],
|
|
17
|
+
"os": ["linux", "darwin"],
|
|
18
|
+
"engines": { "node": ">=14" },
|
|
19
|
+
"keywords": ["claude-code", "tmux", "ai-agents", "terminal-multiplexer", "cmux", "wmux", "agent-orchestration"],
|
|
20
|
+
"author": "Daniel Bratschke",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/DanielIshi/lmux.git"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/DanielIshi/lmux#readme",
|
|
27
|
+
"bugs": { "url": "https://github.com/DanielIshi/lmux/issues" }
|
|
28
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: using-lmux
|
|
3
|
+
description: >
|
|
4
|
+
Use lmux to orchestrate Claude Code agents in tmux panes on Linux.
|
|
5
|
+
Trigger on: lmux operations, coordinating agents in tmux, Linux multi-agent
|
|
6
|
+
sessions, lmux send/read/split commands.
|
|
7
|
+
allowed-tools:
|
|
8
|
+
- Bash
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Using lmux
|
|
12
|
+
|
|
13
|
+
`lmux` is a thin tmux wrapper that mirrors the `cmux` CLI so Claude Code can drive panes on
|
|
14
|
+
Linux servers exactly the same way it drives them on macOS. Use this skill whenever you need
|
|
15
|
+
to spin up, talk to, or read from agents running in tmux.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- `tmux >= 3.2` installed on the host.
|
|
20
|
+
- `lmux` on `$PATH` (the `lmux`, `lmux-send`, `lmux-read`, `lmux-send-key` wrappers).
|
|
21
|
+
- Running inside a tmux session (`$TMUX` set) for surface refs to resolve.
|
|
22
|
+
|
|
23
|
+
If `$TMUX` is unset, start one first:
|
|
24
|
+
```bash
|
|
25
|
+
lmux new-session -s agents
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Surface References
|
|
29
|
+
|
|
30
|
+
A "surface" is a tmux pane addressed as `session:window.pane` or shorthand `session:N` (pane
|
|
31
|
+
index inside the active window). Discover them with:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
lmux tree --all --json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Returns a JSON tree of sessions → windows → panes with their surface refs and current
|
|
38
|
+
command. Use this to pick a target before sending input.
|
|
39
|
+
|
|
40
|
+
## Core Commands
|
|
41
|
+
|
|
42
|
+
### List
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
lmux list-workspaces # all tmux sessions
|
|
46
|
+
lmux tree --all --json # full hierarchy as JSON
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Split / new pane
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
lmux new-split right # split current pane to the right
|
|
53
|
+
lmux new-split down # split downwards
|
|
54
|
+
lmux new-session -s mywork # brand-new session
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Send a command
|
|
58
|
+
|
|
59
|
+
`lmux-send` writes literal text to a pane. **You must include the trailing `\n`** — without
|
|
60
|
+
it the shell will not execute the line.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
lmux-send --surface agents:1 "ls -la\n"
|
|
64
|
+
lmux-send --surface agents:2 "claude --print 'summarize ./README.md'\n"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Read pane output
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
lmux-read --surface agents:1 # current visible buffer
|
|
71
|
+
lmux-read --surface agents:1 --lines 200 # last 200 lines of scrollback
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Send a key
|
|
75
|
+
|
|
76
|
+
For control keys and special keys, use `lmux-send-key` (not `lmux-send`):
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
lmux-send-key --surface agents:1 return
|
|
80
|
+
lmux-send-key --surface agents:1 c-c # Ctrl-C
|
|
81
|
+
lmux-send-key --surface agents:1 c-d # Ctrl-D (EOF / exit)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Orchestration Pattern
|
|
85
|
+
|
|
86
|
+
A typical multi-agent run looks like this:
|
|
87
|
+
|
|
88
|
+
1. **Bootstrap panes.** One `lmux new-split` per agent; label by setting the pane title:
|
|
89
|
+
```bash
|
|
90
|
+
lmux-send --surface agents:2 $'printf "\\033]2;researcher\\007"\n'
|
|
91
|
+
```
|
|
92
|
+
2. **Dispatch.** Send each agent a one-shot Claude invocation wrapped in markers so output
|
|
93
|
+
capture is deterministic:
|
|
94
|
+
```bash
|
|
95
|
+
lmux-send --surface agents:2 \
|
|
96
|
+
"echo '=== START r1 ===' && claude --print 'task A' && echo '=== END r1 ==='\n"
|
|
97
|
+
```
|
|
98
|
+
3. **Poll.** Loop on `lmux-read` every ~2 seconds; stop when `=== END r1 ===` appears or you
|
|
99
|
+
hit a timeout.
|
|
100
|
+
4. **Collect.** Slice the buffer between the START/END markers, strip ANSI, hand the result
|
|
101
|
+
back to the orchestrator.
|
|
102
|
+
5. **Aggregate.** Merge per-pane results into a single summary for the user.
|
|
103
|
+
|
|
104
|
+
## Newline & Quoting Rules
|
|
105
|
+
|
|
106
|
+
- `lmux-send` does **not** add a newline. Always end commands with `\n`.
|
|
107
|
+
- Use double quotes around the payload so `\n` is interpreted by the shell.
|
|
108
|
+
- For payloads that contain literal `$` or backticks, prefer single quotes and embed `\n` via
|
|
109
|
+
`$'...\n'` (Bash ANSI-C quoting):
|
|
110
|
+
```bash
|
|
111
|
+
lmux-send --surface agents:1 $'echo "$HOME"\n'
|
|
112
|
+
```
|
|
113
|
+
- `lmux-send-key` takes a key name (e.g. `return`, `c-c`, `tab`, `up`), not raw text.
|
|
114
|
+
|
|
115
|
+
## Examples
|
|
116
|
+
|
|
117
|
+
### Two parallel researchers
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
lmux new-split right
|
|
121
|
+
lmux new-split down
|
|
122
|
+
# pane indices are now agents:1 (orchestrator), agents:2, agents:3
|
|
123
|
+
|
|
124
|
+
lmux-send --surface agents:2 \
|
|
125
|
+
"echo '=== START a ===' && claude --print 'summarize repo X' && echo '=== END a ==='\n"
|
|
126
|
+
lmux-send --surface agents:3 \
|
|
127
|
+
"echo '=== START b ===' && claude --print 'summarize repo Y' && echo '=== END b ==='\n"
|
|
128
|
+
|
|
129
|
+
# Poll
|
|
130
|
+
for pane in agents:2 agents:3; do
|
|
131
|
+
until lmux-read --surface "$pane" | grep -q "=== END "; do sleep 2; done
|
|
132
|
+
done
|
|
133
|
+
|
|
134
|
+
# Collect
|
|
135
|
+
lmux-read --surface agents:2 --lines 500 \
|
|
136
|
+
| sed -n '/=== START a ===/,/=== END a ===/p'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Cancel a runaway agent
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
lmux-send-key --surface agents:2 c-c
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Tear down a session
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
lmux-send-key --surface agents:1 c-d # exit shell in pane 1
|
|
149
|
+
# or, kill the whole session:
|
|
150
|
+
tmux kill-session -t agents
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Tips
|
|
154
|
+
|
|
155
|
+
- Keep one orchestrator pane that runs `claude` interactively and lets it drive the rest via
|
|
156
|
+
these commands. It scales much better than juggling sessions by hand.
|
|
157
|
+
- Always wrap fan-out work in START/END markers. Scrollback-based completion detection
|
|
158
|
+
without markers is fragile.
|
|
159
|
+
- `lmux tree --all --json` is cheap — re-run it whenever pane indices might have shifted
|
|
160
|
+
(e.g. after a `kill-pane`).
|
|
161
|
+
- For cross-server work, run lmux inside a tmux session on the remote host and drive it over
|
|
162
|
+
SSH; the surface refs are local to that host's tmux server.
|