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.
- {luv_cli-0.2.0 → luv_cli-0.3.0}/PKG-INFO +43 -13
- {luv_cli-0.2.0 → luv_cli-0.3.0}/README.md +42 -12
- {luv_cli-0.2.0 → luv_cli-0.3.0}/docs/configuration.md +23 -0
- {luv_cli-0.2.0 → luv_cli-0.3.0}/docs/remote-sessions.md +141 -21
- {luv_cli-0.2.0 → luv_cli-0.3.0}/docs/sessions.md +85 -27
- {luv_cli-0.2.0 → luv_cli-0.3.0}/luv/__init__.py +972 -159
- {luv_cli-0.2.0 → luv_cli-0.3.0}/pyproject.toml +1 -1
- {luv_cli-0.2.0 → luv_cli-0.3.0}/tests/test_agents.py +601 -23
- {luv_cli-0.2.0 → luv_cli-0.3.0}/.claude/settings.json +0 -0
- {luv_cli-0.2.0 → luv_cli-0.3.0}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.2.0 → luv_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.2.0 → luv_cli-0.3.0}/.gitignore +0 -0
- {luv_cli-0.2.0 → luv_cli-0.3.0}/LICENSE +0 -0
- {luv_cli-0.2.0 → luv_cli-0.3.0}/claude-output.txt +0 -0
- {luv_cli-0.2.0 → luv_cli-0.3.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luv-cli
|
|
3
|
-
Version: 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
|
|
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
|
|
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
|
|
228
|
-
luv rm luv-myrepo-42
|
|
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
|
|
244
|
-
|
|
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
|
|
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
|
|
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
|
|
205
|
-
luv rm luv-myrepo-42
|
|
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
|
|
221
|
-
|
|
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
|
|
214
|
-
directory, then offers to set your default GitHub org. It
|
|
215
|
-
the remote for `tmux`, `luv`, `gh`, and `git` and telling
|
|
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,
|
|
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
|
|
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
|
-
|
|
266
|
-
`luv-
|
|
267
|
-
`
|
|
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
|
|
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
|
|
344
|
-
|
|
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**
|