luv-cli 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luv-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Launch Claude Code or Codex agents on GitHub repos with isolated workspaces and optional Docker dev environments
5
5
  Project-URL: Homepage, https://github.com/exospherehost/luv
6
6
  Project-URL: Repository, https://github.com/exospherehost/luv
@@ -76,7 +76,7 @@ luv -r my-repo 42
76
76
  luv --clean
77
77
 
78
78
  # Tear down a finished session: kill its tmux, delete its workspace
79
- luv rm myrepo-42
79
+ luv rm myrepo-mbp-42
80
80
  luv rm --merged # everything whose PR has landed
81
81
 
82
82
  # See what's running on your remote machine, and reattach
@@ -86,12 +86,14 @@ luv continue
86
86
 
87
87
  ## How it works
88
88
 
89
- 1. Clones the repo into `~/prs/{repo}-{number}/`
90
- 2. Creates a new branch `luv-{number}`
89
+ 1. Clones the repo into `~/prs/{repo}-{machine}-{number}/`
90
+ 2. Creates a new branch `luv-{machine}-{number}`
91
91
  3. Configures the selected agent with the workspace's PR conventions
92
92
  4. Launches Claude with Opus 5 at max effort, or Codex in YOLO mode
93
93
 
94
- All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
94
+ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter, and `{machine}` is a short name for the machine that created the workspace — by default the hostname, or whatever you set with `luv config set machine mbp`.
95
+
96
+ The machine name is what keeps two computers apart. Each one works out the next number from GitHub independently, so your laptop and your box will happily pick the same one; without the slug they would both push a branch called `luv-43`. Folders and branches created before this keep working unchanged.
95
97
 
96
98
  With a remote host configured, steps 1–4 happen on that machine inside a tmux session instead — see [Remote sessions](#remote-sessions).
97
99
 
@@ -103,8 +105,9 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
103
105
  | `luv config set\|get\|unset <key> [value]` | Read or write a single setting |
104
106
  | `luv config list` | Show all settings |
105
107
  | `luv --init` | Configure default GitHub org only |
106
- | `luv ls [--host H] [--prune] [--no-pr]` | List live sessions across hosts, with each one's PR link |
108
+ | `luv ls [--host H] [--prune] [--no-pr]` | List every live session on every host, with each one's PR link |
107
109
  | `luv continue [<repo> [number]]` | Attach to a live session |
110
+ | `luv handover [<repo> [n]] --to HOST` | Move a session to another machine and resume it there |
108
111
  | `luv rm <session\|workspace>...` | Kill a session and delete its workspace on its host |
109
112
  | `luv rm --merged [--host H] [-f]` | Remove every session whose PR is merged |
110
113
  | `luv rm --dead [--host H] [-f]` | Remove workspaces with no live session |
@@ -134,9 +137,20 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
134
137
  | `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
135
138
  | `-i PATH` | SSH identity file to use for this invocation |
136
139
  | `--local` | Force local execution even when a remote host is configured |
137
- | `-f`, `--force` | Skip safety checks (with `--clean`) |
140
+ | `-f`, `--force` | Skip safety checks (with `--clean`); replace an existing destination folder (with `handover`) |
138
141
  | `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
139
142
 
143
+ Handover flags:
144
+
145
+ | Flag | Description |
146
+ |------|-------------|
147
+ | `--to HOST` | Destination machine; `local` for the one you're on |
148
+ | `--from HOST` | Source machine, when luv can't tell from its session registry |
149
+ | `--no-agent-state` | Move the workspace only; the agent starts a new conversation |
150
+ | `--no-attach` | Leave the destination session running detached |
151
+ | `--purge` | Delete the source folder once the copy is verified |
152
+ | `-y` | Skip the "an agent may still be running" confirmation |
153
+
140
154
  ## Docker dev environments
141
155
 
142
156
  If a repo contains `.luv/settings.json` with a `compose_file` key, `luv` automatically starts a Docker Compose environment and runs Claude inside the `dev-environment` container.
@@ -179,7 +193,7 @@ The `dev-environment` service **must** have the selected agent CLI (`claude` or
179
193
 
180
194
  1. Detects `.luv/settings.json` with `compose_file` key
181
195
  2. Tears down any stale environment from a previous run
182
- 3. Starts `docker compose up -d --build` with a unique project name (`luv-{repo}-{number}`) for network/volume isolation
196
+ 3. Starts `docker compose up -d --build` with a unique project name (`luv-{repo}-{machine}-{number}`) for network/volume isolation
183
197
  4. Verifies the `dev-environment` service is running
184
198
  5. Runs the selected agent inside the container via `docker compose exec`
185
199
  6. The repo is volume-mounted, so all file changes and git commits are visible on the host
@@ -201,8 +215,24 @@ luv continue # reattach exactly where you left off
201
215
 
202
216
  `luv` re-invokes itself on the remote over SSH, so every flag works there unchanged. The remote needs `luv`, `tmux`, `gh`, and `git` on its `PATH`.
203
217
 
218
+ `luv ls` lists what is running on every host it knows about, not only the sessions this machine started — a session dispatched from your laptop shows up on your desktop, and `luv continue` and `luv rm` reach it from either.
219
+
204
220
  Use `--local` for a one-off local run, `-s HOST` to target a different machine, and `-i PATH` for a different SSH key.
205
221
 
222
+ ### Handing a session to another machine
223
+
224
+ Start something on your laptop, then move it — workspace, uncommitted work, and the agent's conversation — to a machine that won't go to sleep:
225
+
226
+ ```bash
227
+ luv --local myrepo "start something" # working locally
228
+ luv handover myrepo 43 --to box # resumes mid-conversation on box
229
+ luv handover myrepo 43 --to local # ...and back again later
230
+ ```
231
+
232
+ The whole folder crosses over, including untracked and gitignored files, so a workspace that ran before the move still runs after it. The agent's transcript moves too, so it picks up where it left off rather than starting fresh. Bytes are relayed through the machine you run the command on, so the two ends never need SSH access to each other.
233
+
234
+ The source folder is left on disk (`--purge` deletes it), and the workspace keeps the name it was created with.
235
+
206
236
  Full guide, including how to prepare a fresh Ubuntu box and set up SSH keys: **[docs/remote-sessions.md](docs/remote-sessions.md)**.
207
237
 
208
238
  ## Workspace cleanup
@@ -224,8 +254,8 @@ delete, and never touches a workspace with a live session. `luv rm` is the
224
254
  opposite — you name what goes, and it goes, on whichever host it lives on.
225
255
 
226
256
  ```bash
227
- luv rm myrepo-42 # kill its tmux session, delete ~/prs/myrepo-42 on its host
228
- luv rm luv-myrepo-42 # the session name works too
257
+ luv rm myrepo-mbp-42 # kill its tmux session, delete the folder on its host
258
+ luv rm luv-myrepo-mbp-42 # the session name works too
229
259
  luv rm --merged # every session whose PR has been merged
230
260
  luv rm --dead # workspaces left behind by sessions that already exited
231
261
  luv rm --dead --host box # scope either selector to one machine
@@ -240,9 +270,9 @@ registry as soon as its tmux session exits, but the clone it was working in
240
270
  stays on the remote disk. Those orphans are invisible to `luv ls` and are
241
271
  usually what is actually filling up the box.
242
272
 
243
- Only folders named `{repo}-{N}` are ever eligible — anything else in the
244
- workspace root is left alone, and a target that doesn't resolve to one is an
245
- error rather than an `rm -rf`.
273
+ Only folders ending in `-{N}` are ever eligible — `{repo}-{machine}-{N}` and
274
+ pre-slug `{repo}-{N}` alike. Anything else in the workspace root is left alone,
275
+ and a target that doesn't resolve to one is an error rather than an `rm -rf`.
246
276
 
247
277
  ## Configuration
248
278
 
@@ -53,7 +53,7 @@ luv -r my-repo 42
53
53
  luv --clean
54
54
 
55
55
  # Tear down a finished session: kill its tmux, delete its workspace
56
- luv rm myrepo-42
56
+ luv rm myrepo-mbp-42
57
57
  luv rm --merged # everything whose PR has landed
58
58
 
59
59
  # See what's running on your remote machine, and reattach
@@ -63,12 +63,14 @@ luv continue
63
63
 
64
64
  ## How it works
65
65
 
66
- 1. Clones the repo into `~/prs/{repo}-{number}/`
67
- 2. Creates a new branch `luv-{number}`
66
+ 1. Clones the repo into `~/prs/{repo}-{machine}-{number}/`
67
+ 2. Creates a new branch `luv-{machine}-{number}`
68
68
  3. Configures the selected agent with the workspace's PR conventions
69
69
  4. Launches Claude with Opus 5 at max effort, or Codex in YOLO mode
70
70
 
71
- All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter to avoid collisions.
71
+ All workspaces live under `~/prs/`. The number comes from the repo's GitHub issue counter, and `{machine}` is a short name for the machine that created the workspace — by default the hostname, or whatever you set with `luv config set machine mbp`.
72
+
73
+ The machine name is what keeps two computers apart. Each one works out the next number from GitHub independently, so your laptop and your box will happily pick the same one; without the slug they would both push a branch called `luv-43`. Folders and branches created before this keep working unchanged.
72
74
 
73
75
  With a remote host configured, steps 1–4 happen on that machine inside a tmux session instead — see [Remote sessions](#remote-sessions).
74
76
 
@@ -80,8 +82,9 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
80
82
  | `luv config set\|get\|unset <key> [value]` | Read or write a single setting |
81
83
  | `luv config list` | Show all settings |
82
84
  | `luv --init` | Configure default GitHub org only |
83
- | `luv ls [--host H] [--prune] [--no-pr]` | List live sessions across hosts, with each one's PR link |
85
+ | `luv ls [--host H] [--prune] [--no-pr]` | List every live session on every host, with each one's PR link |
84
86
  | `luv continue [<repo> [number]]` | Attach to a live session |
87
+ | `luv handover [<repo> [n]] --to HOST` | Move a session to another machine and resume it there |
85
88
  | `luv rm <session\|workspace>...` | Kill a session and delete its workspace on its host |
86
89
  | `luv rm --merged [--host H] [-f]` | Remove every session whose PR is merged |
87
90
  | `luv rm --dead [--host H] [-f]` | Remove workspaces with no live session |
@@ -111,9 +114,20 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
111
114
  | `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
112
115
  | `-i PATH` | SSH identity file to use for this invocation |
113
116
  | `--local` | Force local execution even when a remote host is configured |
114
- | `-f`, `--force` | Skip safety checks (with `--clean`) |
117
+ | `-f`, `--force` | Skip safety checks (with `--clean`); replace an existing destination folder (with `handover`) |
115
118
  | `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
116
119
 
120
+ Handover flags:
121
+
122
+ | Flag | Description |
123
+ |------|-------------|
124
+ | `--to HOST` | Destination machine; `local` for the one you're on |
125
+ | `--from HOST` | Source machine, when luv can't tell from its session registry |
126
+ | `--no-agent-state` | Move the workspace only; the agent starts a new conversation |
127
+ | `--no-attach` | Leave the destination session running detached |
128
+ | `--purge` | Delete the source folder once the copy is verified |
129
+ | `-y` | Skip the "an agent may still be running" confirmation |
130
+
117
131
  ## Docker dev environments
118
132
 
119
133
  If a repo contains `.luv/settings.json` with a `compose_file` key, `luv` automatically starts a Docker Compose environment and runs Claude inside the `dev-environment` container.
@@ -156,7 +170,7 @@ The `dev-environment` service **must** have the selected agent CLI (`claude` or
156
170
 
157
171
  1. Detects `.luv/settings.json` with `compose_file` key
158
172
  2. Tears down any stale environment from a previous run
159
- 3. Starts `docker compose up -d --build` with a unique project name (`luv-{repo}-{number}`) for network/volume isolation
173
+ 3. Starts `docker compose up -d --build` with a unique project name (`luv-{repo}-{machine}-{number}`) for network/volume isolation
160
174
  4. Verifies the `dev-environment` service is running
161
175
  5. Runs the selected agent inside the container via `docker compose exec`
162
176
  6. The repo is volume-mounted, so all file changes and git commits are visible on the host
@@ -178,8 +192,24 @@ luv continue # reattach exactly where you left off
178
192
 
179
193
  `luv` re-invokes itself on the remote over SSH, so every flag works there unchanged. The remote needs `luv`, `tmux`, `gh`, and `git` on its `PATH`.
180
194
 
195
+ `luv ls` lists what is running on every host it knows about, not only the sessions this machine started — a session dispatched from your laptop shows up on your desktop, and `luv continue` and `luv rm` reach it from either.
196
+
181
197
  Use `--local` for a one-off local run, `-s HOST` to target a different machine, and `-i PATH` for a different SSH key.
182
198
 
199
+ ### Handing a session to another machine
200
+
201
+ Start something on your laptop, then move it — workspace, uncommitted work, and the agent's conversation — to a machine that won't go to sleep:
202
+
203
+ ```bash
204
+ luv --local myrepo "start something" # working locally
205
+ luv handover myrepo 43 --to box # resumes mid-conversation on box
206
+ luv handover myrepo 43 --to local # ...and back again later
207
+ ```
208
+
209
+ The whole folder crosses over, including untracked and gitignored files, so a workspace that ran before the move still runs after it. The agent's transcript moves too, so it picks up where it left off rather than starting fresh. Bytes are relayed through the machine you run the command on, so the two ends never need SSH access to each other.
210
+
211
+ The source folder is left on disk (`--purge` deletes it), and the workspace keeps the name it was created with.
212
+
183
213
  Full guide, including how to prepare a fresh Ubuntu box and set up SSH keys: **[docs/remote-sessions.md](docs/remote-sessions.md)**.
184
214
 
185
215
  ## Workspace cleanup
@@ -201,8 +231,8 @@ delete, and never touches a workspace with a live session. `luv rm` is the
201
231
  opposite — you name what goes, and it goes, on whichever host it lives on.
202
232
 
203
233
  ```bash
204
- luv rm myrepo-42 # kill its tmux session, delete ~/prs/myrepo-42 on its host
205
- luv rm luv-myrepo-42 # the session name works too
234
+ luv rm myrepo-mbp-42 # kill its tmux session, delete the folder on its host
235
+ luv rm luv-myrepo-mbp-42 # the session name works too
206
236
  luv rm --merged # every session whose PR has been merged
207
237
  luv rm --dead # workspaces left behind by sessions that already exited
208
238
  luv rm --dead --host box # scope either selector to one machine
@@ -217,9 +247,9 @@ registry as soon as its tmux session exits, but the clone it was working in
217
247
  stays on the remote disk. Those orphans are invisible to `luv ls` and are
218
248
  usually what is actually filling up the box.
219
249
 
220
- Only folders named `{repo}-{N}` are ever eligible — anything else in the
221
- workspace root is left alone, and a target that doesn't resolve to one is an
222
- error rather than an `rm -rf`.
250
+ Only folders ending in `-{N}` are ever eligible — `{repo}-{machine}-{N}` and
251
+ pre-slug `{repo}-{N}` alike. Anything else in the workspace root is left alone,
252
+ and a target that doesn't resolve to one is an error rather than an `rm -rf`.
223
253
 
224
254
  ## Configuration
225
255
 
@@ -43,6 +43,28 @@ want for hosts, paths, and org names.
43
43
  The **laptop** resolves this before dispatching, so a remote machine never needs
44
44
  its own `org` configured.
45
45
 
46
+ ### `machine`
47
+
48
+ *string, default: this machine's hostname.* A short name for **this** machine,
49
+ used in the workspace folder (`myrepo-mbp-43`) and branch (`luv-mbp-43`) of
50
+ everything created here.
51
+
52
+ It exists because every machine works out the next workspace number from
53
+ GitHub's issue counter independently, so a laptop and a box will happily pick
54
+ the same one — and without the slug they would push the same branch. The value
55
+ is lowercased, stripped to letters and digits, and capped at 8 characters, so
56
+ `Niveds-MacBook.local` becomes `nivedsma`; set it explicitly for something
57
+ readable:
58
+
59
+ ```bash
60
+ luv config set machine mbp
61
+ ```
62
+
63
+ The slug is stamped when a workspace is created and never changes afterwards —
64
+ `luv handover` moves `myrepo-mbp-43` to another machine under that same name,
65
+ because it records where the workspace came from and its branch may already be
66
+ pushed. Workspaces created before slugs existed keep working.
67
+
46
68
  ### `prs_dir`
47
69
 
48
70
  *string, default `~/prs`.* Where workspace folders are created on **this**
@@ -107,6 +129,7 @@ those combinations contradict each other.
107
129
  ```json
108
130
  {
109
131
  "org": "exosphere",
132
+ "machine": "mbp",
110
133
  "prs_dir": "~/prs",
111
134
  "remote": {
112
135
  "host": "box",
@@ -9,6 +9,7 @@ still up.
9
9
  - [Preparing a new remote machine (Ubuntu)](#preparing-a-new-remote-machine-ubuntu)
10
10
  - [Configuring luv](#configuring-luv)
11
11
  - [Daily use](#daily-use)
12
+ - [Handing a session to another machine](#handing-a-session-to-another-machine)
12
13
  - [Session lifecycle](#session-lifecycle)
13
14
  - [Session naming and identity](#session-naming-and-identity)
14
15
  - [What runs where](#what-runs-where)
@@ -28,9 +29,9 @@ luv myrepo "fix the bug"
28
29
  └─ ssh -t box 'bash -lc "…"' ────► tmux new-session -A -s luv-pending-3f9a
29
30
  └─ luv exosphere/myrepo "fix the bug"
30
31
  ├─ gh api → next number = 42
31
- ├─ git clone → ~/prs/myrepo-42
32
- ├─ git checkout -b luv-42
33
- ├─ tmux rename-session → luv-myrepo-42
32
+ ├─ git clone → ~/prs/myrepo-box-42
33
+ ├─ git checkout -b luv-box-42
34
+ ├─ tmux rename-session → luv-myrepo-box-42
34
35
  └─ exec claude …
35
36
  (session keeps running after you disconnect)
36
37
  ```
@@ -210,19 +211,24 @@ On your laptop:
210
211
  luv config
211
212
  ```
212
213
 
213
- The wizard asks for the host, the SSH identity file, and the remote workspace
214
- directory, then offers to set your default GitHub org. It finishes by checking
215
- the remote for `tmux`, `luv`, `gh`, and `git` and telling you exactly what's
216
- missing.
214
+ The wizard asks for this machine's name, the host, the SSH identity file, and
215
+ the remote workspace directory, then offers to set your default GitHub org. It
216
+ finishes by checking the remote for `tmux`, `luv`, `gh`, and `git` and telling
217
+ you exactly what's missing.
217
218
 
218
219
  Non-interactively:
219
220
 
220
221
  ```bash
222
+ luv config set machine mbp
221
223
  luv config set remote.host box
222
224
  luv config set remote.identity_file ~/.ssh/id_ed25519
223
225
  luv config list
224
226
  ```
225
227
 
228
+ Setting `machine` on each machine you use is worth the ten seconds: it is the
229
+ label that ends up in every workspace folder and branch name, and the hostname
230
+ default is rarely the nicest one.
231
+
226
232
  Full key reference: [configuration.md](configuration.md).
227
233
 
228
234
  ## Daily use
@@ -233,7 +239,7 @@ about how you use luv changes:
233
239
  ```bash
234
240
  luv myrepo "fix the flaky test" # new workspace on the remote
235
241
  luv myrepo 42 "keep going" # reopen workspace 42 there
236
- luv ls # what's running, across all hosts
242
+ luv ls # what's running, on every host, whoever started it
237
243
  luv continue # attach to a session
238
244
  luv continue myrepo # attach to the newest myrepo session
239
245
  luv continue myrepo 42 # attach to a specific workspace
@@ -250,36 +256,116 @@ luv -i ~/.ssh/other_key myrepo # use a different key this once
250
256
  Detach from a session with `Ctrl-b d` (tmux's default prefix). The agent keeps
251
257
  running.
252
258
 
259
+ ## Handing a session to another machine
260
+
261
+ `luv handover` relocates a running workspace. The usual direction is laptop →
262
+ box: you start something locally, then decide it should keep running somewhere
263
+ that doesn't sleep.
264
+
265
+ ```bash
266
+ luv --local myrepo "start something" # → ~/prs/myrepo-mbp-43, branch luv-mbp-43
267
+ luv handover myrepo 43 --to box # attaches to it, now running on box
268
+ luv handover myrepo 43 --to local # and back down again later
269
+ ```
270
+
271
+ ```
272
+ LAPTOP (the one you run the command on)
273
+ │
274
+ ssh box tar -c ─┤─ ssh gpu tar -x ← bytes are relayed through here, so
275
+ │ box and gpu need no keys for each other
276
+ BOX GPU-BOX
277
+ tmux kill-session luv-myrepo-mbp-43
278
+ docker compose -p … down
279
+ ~/prs/myrepo-mbp-43 ───────────────► ~/prs/myrepo-mbp-43 (whole folder)
280
+ ~/.claude/projects/-home-u-prs-… ───► ~/.claude/projects/… (transcript)
281
+ (folder left in place) tmux new-session -A -s luv-myrepo-mbp-43
282
+ └─ luv exo/myrepo 43 -r → claude --resume
283
+ ```
284
+
285
+ **Everything crosses over.** The folder is copied byte for byte — `.git`, staged
286
+ and unstaged edits, untracked files, and gitignored ones like `.env` and
287
+ `node_modules`. A workspace that ran before the move still runs after it.
288
+
289
+ **So does the conversation.** Claude's transcript
290
+ (`~/.claude/projects/<path-slug>/*.jsonl`) and Codex's rollouts
291
+ (`~/.codex/sessions/**/rollout-*.jsonl`) travel with the workspace, and the
292
+ absolute path recorded inside them is rewritten for the new machine — the two
293
+ rarely agree on it. The destination then starts with `-r`, so `claude --resume`
294
+ or `codex resume --last` continues the thread rather than opening a new one.
295
+ This part depends on each agent's on-disk layout, so luv says what it found and
296
+ carries on with a fresh conversation if it finds nothing; `--no-agent-state`
297
+ skips it deliberately.
298
+
299
+ **The agent is briefly down.** luv checks everything it can first — the
300
+ destination is reachable, has luv, and has no folder in the way — then stops the
301
+ agent, because copying a workspace out from under a running one is the only way
302
+ to get a torn tree. Killing the tmux session SIGHUPs the pane, so luv also tears
303
+ the Docker environment down explicitly rather than relying on the agent's exit.
304
+
305
+ **Nothing is deleted.** The source folder stays on disk, so a failed transfer
306
+ costs nothing: luv prints the command to restart the session where it was. Use
307
+ `--purge` to delete it once the copy verifies, or `luv --clean` on that machine
308
+ later.
309
+
310
+ **The name doesn't change.** `myrepo-mbp-43` stays `myrepo-mbp-43` on the box —
311
+ the slug records where a workspace came from, and its branch may already be
312
+ pushed. That is also what makes the move safe: a workspace created on the box
313
+ would be `myrepo-box-43`, so the two can sit side by side.
314
+
315
+ Handover works on a workspace whose agent has already exited, too — name the
316
+ machine with `--from` and luv will move the folder and start it fresh.
317
+
318
+ | Flag | |
319
+ |---|---|
320
+ | `--to HOST` | Destination; `local` for the machine you're on. Required. |
321
+ | `--from HOST` | Source, when luv's registry doesn't already know |
322
+ | `--no-agent-state` | Move the workspace only |
323
+ | `--no-attach` | Leave the new session detached |
324
+ | `--purge` | Delete the source folder after the copy verifies |
325
+ | `-f` | Replace an existing folder on the destination |
326
+ | `-y` | Skip the "an agent may still be running" confirmation |
327
+
253
328
  ## Session lifecycle
254
329
 
255
330
  | Step | What happens |
256
331
  |---|---|
257
332
  | `luv myrepo "…"` | Laptop records a registry entry, opens SSH, creates the tmux session, remote luv clones and launches the agent |
258
333
  | `Ctrl-b d` or lost connection | tmux session keeps running; agent unaffected; Docker containers stay up |
259
- | `luv ls` | Laptop queries each host's tmux and refreshes the registry |
334
+ | `luv ls` | Laptop queries every known host's tmux and refreshes the registry, adopting sessions another machine started |
260
335
  | `luv continue` | Reattaches; other clients are detached so the pane isn't size-clamped |
261
336
  | Agent exits | The pane's command ends, tmux session disappears, Docker environment is torn down, `luv ls` prunes the entry on its next run |
262
337
 
263
338
  ## Session naming and identity
264
339
 
265
- Sessions are named after the workspace folder: `~/prs/myrepo-42` becomes
266
- `luv-myrepo-42`. tmux forbids `.` and `:` in session names, so a repo like
267
- `foo.js` becomes `luv-foo_js-42`.
340
+ Workspace folders are `{repo}-{machine}-{number}` and branches
341
+ `luv-{machine}-{number}`, where `{machine}` names whichever machine created the
342
+ workspace (config key `machine`, defaulting to the hostname). Each machine works
343
+ out the next number from GitHub's issue counter on its own, so without that slug
344
+ a laptop and a box would routinely pick the same one and push the same branch.
345
+ Folders and branches created before slugs existed keep working — luv looks for
346
+ its own name first, then any other machine's, then the plain `{repo}-{number}`.
347
+
348
+ Sessions are named after the workspace folder: `~/prs/myrepo-box-42` becomes
349
+ `luv-myrepo-box-42`. tmux forbids `.` and `:` in session names, so a repo like
350
+ `foo.js` becomes `luv-foo_js-box-42`.
268
351
 
269
352
  When the workspace is already known — reopening by number, `-pr`, or `-l` — the
270
- laptop uses that name directly and `tmux new-session -A` doubles as "attach if
271
- it's already running".
353
+ laptop pins the session name down before dispatching, so `tmux new-session -A`
354
+ doubles as "attach if it's already running". It can't compute that name, though,
355
+ since the slug belongs to the machine holding the folder: it takes it from the
356
+ session registry, or asks the host directly, and falls back to the
357
+ `luv-pending-<id>` rename-on-arrival path if the host doesn't answer.
272
358
 
273
359
  When it isn't known — a brand new workspace, or bare `-n`/`-r` — the laptop uses
274
360
  a placeholder `luv-pending-<id>` and the remote renames it once the clone lands.
275
361
  If the target name is already taken by another live session, luv keeps the
276
- session under `luv-myrepo-42-2` and warns rather than failing.
362
+ session under `luv-myrepo-box-42-2` and warns rather than failing.
277
363
 
278
364
  Each session also carries two tmux options, which is what `luv ls` reads:
279
365
 
280
366
  ```bash
281
- tmux show-options -t luv-myrepo-42 -v @luv_id
282
- tmux show-options -t luv-myrepo-42 -v @luv_workspace
367
+ tmux show-options -t luv-myrepo-box-42 -v @luv_id
368
+ tmux show-options -t luv-myrepo-box-42 -v @luv_workspace
283
369
  ```
284
370
 
285
371
  ## What runs where
@@ -288,6 +374,7 @@ tmux show-options -t luv-myrepo-42 -v @luv_workspace
288
374
  |---|---|---|
289
375
  | `luv config`, `luv --init` | Always local | – |
290
376
  | `luv ls`, `luv continue` | Local, queries remote over SSH | – |
377
+ | `luv handover` | Local, drives both machines over SSH | starts one on the destination |
291
378
  | `luv <repo> [prompt]` | Remote | yes |
292
379
  | `luv <repo> <n>`, `-pr`, `-l` | Remote | yes |
293
380
  | `luv <repo> -n` / `-r` | Remote | yes |
@@ -304,7 +391,7 @@ invocation, `docker compose up` runs *inside* the pane:
304
391
 
305
392
  - Detaching leaves the containers running.
306
393
  - Teardown happens when the agent exits, not when your SSH client goes away.
307
- - The compose project name is still `luv-{repo}-{number}`, so two workspaces on
394
+ - The compose project name is still `luv-{repo}-{machine}-{number}`, so two workspaces on
308
395
  the same repo don't collide.
309
396
 
310
397
  Install Docker on the remote and add your user to the `docker` group:
@@ -340,14 +427,47 @@ discards the command — so the prompt is dropped. luv prints a note when this c
340
427
  happen. Type the prompt into the running agent instead.
341
428
 
342
429
  **`luv ls` says a host is unreachable**
343
- It shows the last known state rather than pruning; nothing is lost. When a host
344
- is gone for good, `luv ls --prune` forgets its entries.
430
+ It shows that host's last known state rather than pruning; nothing is lost. A
431
+ configured host with no entries yet has no state to show, so it just says its
432
+ sessions aren't listed. When a host is gone for good, `luv ls --prune` forgets
433
+ its entries — and `luv config unset remote.hosts.<name>` stops the scan.
434
+
435
+ **Junk like `35;22;1M` appears at my prompt after a dropped connection**
436
+
437
+ That's mouse-tracking reports. The remote tmux/agent turned mouse tracking on
438
+ in *your* terminal and, having been killed along with the connection, never
439
+ turned it off — so every mouse move now types coordinates at your shell.
440
+ Bracketed paste (`200~` around pastes), a missing cursor, and a stuck
441
+ alternate screen come from the same cause.
442
+
443
+ luv now cleans this up itself: it stays alive as the parent of `ssh` and
444
+ restores the terminal whatever happens to the connection. If you land in this
445
+ state from something else, `reset` (or `stty sane` plus
446
+ `printf '\033[?1003l\033[?1006l\033[?2004l'`) clears it.
447
+
448
+ **Handover says the destination already has that folder**
449
+ Something with the same name is already there — either the same workspace from
450
+ an earlier handover, or one created on the machine that made this one. Look at
451
+ it first (`ssh box ls ~/prs`), then either `luv --clean` it or pass `-f` to
452
+ replace it.
453
+
454
+ **After a handover the agent started a fresh conversation**
455
+ luv prints a warning when it can't find a transcript for the workspace. The
456
+ usual cause is the workspace having been opened under a different path than the
457
+ one the agent recorded, or a Codex rollout stored somewhere other than
458
+ `~/.codex/sessions`. The workspace itself is unaffected — the code, branch, and
459
+ uncommitted work all moved.
460
+
461
+ **Handover left the session down**
462
+ The source folder is never deleted without `--purge`, so restart it where it
463
+ was: `luv <repo> <n> -r -s <source-host>`. luv prints that exact command before
464
+ it starts copying.
345
465
 
346
466
  **A session is wedged**
347
467
 
348
468
  ```bash
349
469
  ssh box tmux ls # see everything, not just luv's
350
- ssh box tmux kill-session -t luv-myrepo-42
470
+ ssh box tmux kill-session -t luv-myrepo-box-42
351
471
  ```
352
472
 
353
473
  **I'm already inside tmux locally**