luv-cli 0.2.1__tar.gz → 0.4.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.1 → luv_cli-0.4.0}/PKG-INFO +63 -16
- {luv_cli-0.2.1 → luv_cli-0.4.0}/README.md +62 -15
- {luv_cli-0.2.1 → luv_cli-0.4.0}/docs/configuration.md +23 -0
- {luv_cli-0.2.1 → luv_cli-0.4.0}/docs/remote-sessions.md +141 -26
- {luv_cli-0.2.1 → luv_cli-0.4.0}/docs/sessions.md +87 -28
- {luv_cli-0.2.1 → luv_cli-0.4.0}/luv/__init__.py +947 -158
- {luv_cli-0.2.1 → luv_cli-0.4.0}/pyproject.toml +1 -1
- {luv_cli-0.2.1 → luv_cli-0.4.0}/tests/test_agents.py +631 -21
- {luv_cli-0.2.1 → luv_cli-0.4.0}/.claude/settings.json +0 -0
- {luv_cli-0.2.1 → luv_cli-0.4.0}/.failproofai/policies-config.json +0 -0
- {luv_cli-0.2.1 → luv_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {luv_cli-0.2.1 → luv_cli-0.4.0}/.gitignore +0 -0
- {luv_cli-0.2.1 → luv_cli-0.4.0}/LICENSE +0 -0
- {luv_cli-0.2.1 → luv_cli-0.4.0}/claude-output.txt +0 -0
- {luv_cli-0.2.1 → luv_cli-0.4.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.4.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
|
|
@@ -63,7 +63,7 @@ luv other-org/my-repo "fix the bug"
|
|
|
63
63
|
# Reopen workspace #42
|
|
64
64
|
luv my-repo 42
|
|
65
65
|
|
|
66
|
-
#
|
|
66
|
+
# Clone any GitHub PR by URL into a fresh workspace
|
|
67
67
|
luv -l https://github.com/org/repo/pull/123
|
|
68
68
|
|
|
69
69
|
# Open a shell instead of Claude
|
|
@@ -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,15 +86,33 @@ 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
|
|
|
100
|
+
### Opening an existing PR
|
|
101
|
+
|
|
102
|
+
`luv -l <PR URL>` gives you the PR as it is *right now*: a full clone into a
|
|
103
|
+
workspace of its own, checked out on the PR's head branch. It never reuses a
|
|
104
|
+
folder that happens to be sitting there — a URL you paste is a request to look
|
|
105
|
+
at that PR, not at whatever was checked out in `myrepo-mbp-123` last week. Run
|
|
106
|
+
it twice on the same PR and you get `myrepo-mbp-123` and `myrepo-mbp-123_2`,
|
|
107
|
+
which are independent workspaces you can run side by side. When the PR comes
|
|
108
|
+
from a fork, the base repo is added as `upstream` and fetched too, so the branch
|
|
109
|
+
you need to diff or rebase against is there.
|
|
110
|
+
|
|
111
|
+
`-r` is the exception: resuming means picking up a conversation, and a
|
|
112
|
+
conversation lives in the folder it was held in, so `luv -l <URL> -r` reopens the
|
|
113
|
+
newest workspace for that PR instead of cloning another. `luv <repo> -pr <N>`
|
|
114
|
+
also keeps reusing its workspace — it names a workspace, where a URL names a PR.
|
|
115
|
+
|
|
98
116
|
## Commands
|
|
99
117
|
|
|
100
118
|
| Command | Description |
|
|
@@ -103,8 +121,9 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
|
|
|
103
121
|
| `luv config set\|get\|unset <key> [value]` | Read or write a single setting |
|
|
104
122
|
| `luv config list` | Show all settings |
|
|
105
123
|
| `luv --init` | Configure default GitHub org only |
|
|
106
|
-
| `luv ls [--host H] [--prune] [--no-pr]` | List live
|
|
124
|
+
| `luv ls [--host H] [--prune] [--no-pr]` | List every live session on every host, with each one's PR link |
|
|
107
125
|
| `luv continue [<repo> [number]]` | Attach to a live session |
|
|
126
|
+
| `luv handover [<repo> [n]] --to HOST` | Move a session to another machine and resume it there |
|
|
108
127
|
| `luv rm <session\|workspace>...` | Kill a session and delete its workspace on its host |
|
|
109
128
|
| `luv rm --merged [--host H] [-f]` | Remove every session whose PR is merged |
|
|
110
129
|
| `luv rm --dead [--host H] [-f]` | Remove workspaces with no live session |
|
|
@@ -112,8 +131,8 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
|
|
|
112
131
|
| `luv --codex [org/]<repo> [prompt...]` | Create a workspace and launch Codex in YOLO mode |
|
|
113
132
|
| `luv [org/]<repo> -b <branch> [prompt...]` | Create a workspace based off `<branch>` instead of the default |
|
|
114
133
|
| `luv [org/]<repo> <number> [prompt]` | Reopen an existing workspace |
|
|
115
|
-
| `luv -l <PR URL> [prompt]` |
|
|
116
|
-
| `luv [org/]<repo> -pr <number> [prompt]` | Open a PR by repo + number |
|
|
134
|
+
| `luv -l <PR URL> [prompt]` | Clone any GitHub PR by URL into a fresh workspace |
|
|
135
|
+
| `luv [org/]<repo> -pr <number> [prompt]` | Open a PR by repo + number, reusing its workspace |
|
|
117
136
|
| `luv --clean` | Delete workspaces where the branch is fully pushed/merged |
|
|
118
137
|
| `luv --clean -f` | Force delete all workspaces |
|
|
119
138
|
| `luv --clean --safe -f` | Force delete only workspaces older than 24h |
|
|
@@ -134,9 +153,20 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
|
|
|
134
153
|
| `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
|
|
135
154
|
| `-i PATH` | SSH identity file to use for this invocation |
|
|
136
155
|
| `--local` | Force local execution even when a remote host is configured |
|
|
137
|
-
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
156
|
+
| `-f`, `--force` | Skip safety checks (with `--clean`); replace an existing destination folder (with `handover`) |
|
|
138
157
|
| `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
|
|
139
158
|
|
|
159
|
+
Handover flags:
|
|
160
|
+
|
|
161
|
+
| Flag | Description |
|
|
162
|
+
|------|-------------|
|
|
163
|
+
| `--to HOST` | Destination machine; `local` for the one you're on |
|
|
164
|
+
| `--from HOST` | Source machine, when luv can't tell from its session registry |
|
|
165
|
+
| `--no-agent-state` | Move the workspace only; the agent starts a new conversation |
|
|
166
|
+
| `--no-attach` | Leave the destination session running detached |
|
|
167
|
+
| `--purge` | Delete the source folder once the copy is verified |
|
|
168
|
+
| `-y` | Skip the "an agent may still be running" confirmation |
|
|
169
|
+
|
|
140
170
|
## Docker dev environments
|
|
141
171
|
|
|
142
172
|
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 +209,7 @@ The `dev-environment` service **must** have the selected agent CLI (`claude` or
|
|
|
179
209
|
|
|
180
210
|
1. Detects `.luv/settings.json` with `compose_file` key
|
|
181
211
|
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
|
|
212
|
+
3. Starts `docker compose up -d --build` with a unique project name (`luv-{repo}-{machine}-{number}`) for network/volume isolation
|
|
183
213
|
4. Verifies the `dev-environment` service is running
|
|
184
214
|
5. Runs the selected agent inside the container via `docker compose exec`
|
|
185
215
|
6. The repo is volume-mounted, so all file changes and git commits are visible on the host
|
|
@@ -201,8 +231,24 @@ luv continue # reattach exactly where you left off
|
|
|
201
231
|
|
|
202
232
|
`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
233
|
|
|
234
|
+
`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.
|
|
235
|
+
|
|
204
236
|
Use `--local` for a one-off local run, `-s HOST` to target a different machine, and `-i PATH` for a different SSH key.
|
|
205
237
|
|
|
238
|
+
### Handing a session to another machine
|
|
239
|
+
|
|
240
|
+
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:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
luv --local myrepo "start something" # working locally
|
|
244
|
+
luv handover myrepo 43 --to box # resumes mid-conversation on box
|
|
245
|
+
luv handover myrepo 43 --to local # ...and back again later
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
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.
|
|
249
|
+
|
|
250
|
+
The source folder is left on disk (`--purge` deletes it), and the workspace keeps the name it was created with.
|
|
251
|
+
|
|
206
252
|
Full guide, including how to prepare a fresh Ubuntu box and set up SSH keys: **[docs/remote-sessions.md](docs/remote-sessions.md)**.
|
|
207
253
|
|
|
208
254
|
## Workspace cleanup
|
|
@@ -224,8 +270,8 @@ delete, and never touches a workspace with a live session. `luv rm` is the
|
|
|
224
270
|
opposite — you name what goes, and it goes, on whichever host it lives on.
|
|
225
271
|
|
|
226
272
|
```bash
|
|
227
|
-
luv rm myrepo-42
|
|
228
|
-
luv rm luv-myrepo-42
|
|
273
|
+
luv rm myrepo-mbp-42 # kill its tmux session, delete the folder on its host
|
|
274
|
+
luv rm luv-myrepo-mbp-42 # the session name works too
|
|
229
275
|
luv rm --merged # every session whose PR has been merged
|
|
230
276
|
luv rm --dead # workspaces left behind by sessions that already exited
|
|
231
277
|
luv rm --dead --host box # scope either selector to one machine
|
|
@@ -240,9 +286,10 @@ registry as soon as its tmux session exits, but the clone it was working in
|
|
|
240
286
|
stays on the remote disk. Those orphans are invisible to `luv ls` and are
|
|
241
287
|
usually what is actually filling up the box.
|
|
242
288
|
|
|
243
|
-
Only folders
|
|
244
|
-
|
|
245
|
-
|
|
289
|
+
Only folders ending in `-{N}` are ever eligible — `{repo}-{machine}-{N}`, a
|
|
290
|
+
`luv -l` copy `{repo}-{machine}-{N}_2`, and pre-slug `{repo}-{N}` alike.
|
|
291
|
+
Anything else in the workspace root is left alone, and a target that doesn't
|
|
292
|
+
resolve to one is an error rather than an `rm -rf`.
|
|
246
293
|
|
|
247
294
|
## Configuration
|
|
248
295
|
|
|
@@ -40,7 +40,7 @@ luv other-org/my-repo "fix the bug"
|
|
|
40
40
|
# Reopen workspace #42
|
|
41
41
|
luv my-repo 42
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Clone any GitHub PR by URL into a fresh workspace
|
|
44
44
|
luv -l https://github.com/org/repo/pull/123
|
|
45
45
|
|
|
46
46
|
# Open a shell instead of Claude
|
|
@@ -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,15 +63,33 @@ 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
|
|
|
77
|
+
### Opening an existing PR
|
|
78
|
+
|
|
79
|
+
`luv -l <PR URL>` gives you the PR as it is *right now*: a full clone into a
|
|
80
|
+
workspace of its own, checked out on the PR's head branch. It never reuses a
|
|
81
|
+
folder that happens to be sitting there — a URL you paste is a request to look
|
|
82
|
+
at that PR, not at whatever was checked out in `myrepo-mbp-123` last week. Run
|
|
83
|
+
it twice on the same PR and you get `myrepo-mbp-123` and `myrepo-mbp-123_2`,
|
|
84
|
+
which are independent workspaces you can run side by side. When the PR comes
|
|
85
|
+
from a fork, the base repo is added as `upstream` and fetched too, so the branch
|
|
86
|
+
you need to diff or rebase against is there.
|
|
87
|
+
|
|
88
|
+
`-r` is the exception: resuming means picking up a conversation, and a
|
|
89
|
+
conversation lives in the folder it was held in, so `luv -l <URL> -r` reopens the
|
|
90
|
+
newest workspace for that PR instead of cloning another. `luv <repo> -pr <N>`
|
|
91
|
+
also keeps reusing its workspace — it names a workspace, where a URL names a PR.
|
|
92
|
+
|
|
75
93
|
## Commands
|
|
76
94
|
|
|
77
95
|
| Command | Description |
|
|
@@ -80,8 +98,9 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
|
|
|
80
98
|
| `luv config set\|get\|unset <key> [value]` | Read or write a single setting |
|
|
81
99
|
| `luv config list` | Show all settings |
|
|
82
100
|
| `luv --init` | Configure default GitHub org only |
|
|
83
|
-
| `luv ls [--host H] [--prune] [--no-pr]` | List live
|
|
101
|
+
| `luv ls [--host H] [--prune] [--no-pr]` | List every live session on every host, with each one's PR link |
|
|
84
102
|
| `luv continue [<repo> [number]]` | Attach to a live session |
|
|
103
|
+
| `luv handover [<repo> [n]] --to HOST` | Move a session to another machine and resume it there |
|
|
85
104
|
| `luv rm <session\|workspace>...` | Kill a session and delete its workspace on its host |
|
|
86
105
|
| `luv rm --merged [--host H] [-f]` | Remove every session whose PR is merged |
|
|
87
106
|
| `luv rm --dead [--host H] [-f]` | Remove workspaces with no live session |
|
|
@@ -89,8 +108,8 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
|
|
|
89
108
|
| `luv --codex [org/]<repo> [prompt...]` | Create a workspace and launch Codex in YOLO mode |
|
|
90
109
|
| `luv [org/]<repo> -b <branch> [prompt...]` | Create a workspace based off `<branch>` instead of the default |
|
|
91
110
|
| `luv [org/]<repo> <number> [prompt]` | Reopen an existing workspace |
|
|
92
|
-
| `luv -l <PR URL> [prompt]` |
|
|
93
|
-
| `luv [org/]<repo> -pr <number> [prompt]` | Open a PR by repo + number |
|
|
111
|
+
| `luv -l <PR URL> [prompt]` | Clone any GitHub PR by URL into a fresh workspace |
|
|
112
|
+
| `luv [org/]<repo> -pr <number> [prompt]` | Open a PR by repo + number, reusing its workspace |
|
|
94
113
|
| `luv --clean` | Delete workspaces where the branch is fully pushed/merged |
|
|
95
114
|
| `luv --clean -f` | Force delete all workspaces |
|
|
96
115
|
| `luv --clean --safe -f` | Force delete only workspaces older than 24h |
|
|
@@ -111,9 +130,20 @@ With a remote host configured, steps 1–4 happen on that machine inside a tmux
|
|
|
111
130
|
| `-s HOST` | Run on `HOST` over SSH, overriding the configured remote host |
|
|
112
131
|
| `-i PATH` | SSH identity file to use for this invocation |
|
|
113
132
|
| `--local` | Force local execution even when a remote host is configured |
|
|
114
|
-
| `-f`, `--force` | Skip safety checks (with `--clean`) |
|
|
133
|
+
| `-f`, `--force` | Skip safety checks (with `--clean`); replace an existing destination folder (with `handover`) |
|
|
115
134
|
| `--safe` | With `--clean -f`, only delete workspaces older than 24h (mtime) |
|
|
116
135
|
|
|
136
|
+
Handover flags:
|
|
137
|
+
|
|
138
|
+
| Flag | Description |
|
|
139
|
+
|------|-------------|
|
|
140
|
+
| `--to HOST` | Destination machine; `local` for the one you're on |
|
|
141
|
+
| `--from HOST` | Source machine, when luv can't tell from its session registry |
|
|
142
|
+
| `--no-agent-state` | Move the workspace only; the agent starts a new conversation |
|
|
143
|
+
| `--no-attach` | Leave the destination session running detached |
|
|
144
|
+
| `--purge` | Delete the source folder once the copy is verified |
|
|
145
|
+
| `-y` | Skip the "an agent may still be running" confirmation |
|
|
146
|
+
|
|
117
147
|
## Docker dev environments
|
|
118
148
|
|
|
119
149
|
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 +186,7 @@ The `dev-environment` service **must** have the selected agent CLI (`claude` or
|
|
|
156
186
|
|
|
157
187
|
1. Detects `.luv/settings.json` with `compose_file` key
|
|
158
188
|
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
|
|
189
|
+
3. Starts `docker compose up -d --build` with a unique project name (`luv-{repo}-{machine}-{number}`) for network/volume isolation
|
|
160
190
|
4. Verifies the `dev-environment` service is running
|
|
161
191
|
5. Runs the selected agent inside the container via `docker compose exec`
|
|
162
192
|
6. The repo is volume-mounted, so all file changes and git commits are visible on the host
|
|
@@ -178,8 +208,24 @@ luv continue # reattach exactly where you left off
|
|
|
178
208
|
|
|
179
209
|
`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
210
|
|
|
211
|
+
`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.
|
|
212
|
+
|
|
181
213
|
Use `--local` for a one-off local run, `-s HOST` to target a different machine, and `-i PATH` for a different SSH key.
|
|
182
214
|
|
|
215
|
+
### Handing a session to another machine
|
|
216
|
+
|
|
217
|
+
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:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
luv --local myrepo "start something" # working locally
|
|
221
|
+
luv handover myrepo 43 --to box # resumes mid-conversation on box
|
|
222
|
+
luv handover myrepo 43 --to local # ...and back again later
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
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.
|
|
226
|
+
|
|
227
|
+
The source folder is left on disk (`--purge` deletes it), and the workspace keeps the name it was created with.
|
|
228
|
+
|
|
183
229
|
Full guide, including how to prepare a fresh Ubuntu box and set up SSH keys: **[docs/remote-sessions.md](docs/remote-sessions.md)**.
|
|
184
230
|
|
|
185
231
|
## Workspace cleanup
|
|
@@ -201,8 +247,8 @@ delete, and never touches a workspace with a live session. `luv rm` is the
|
|
|
201
247
|
opposite — you name what goes, and it goes, on whichever host it lives on.
|
|
202
248
|
|
|
203
249
|
```bash
|
|
204
|
-
luv rm myrepo-42
|
|
205
|
-
luv rm luv-myrepo-42
|
|
250
|
+
luv rm myrepo-mbp-42 # kill its tmux session, delete the folder on its host
|
|
251
|
+
luv rm luv-myrepo-mbp-42 # the session name works too
|
|
206
252
|
luv rm --merged # every session whose PR has been merged
|
|
207
253
|
luv rm --dead # workspaces left behind by sessions that already exited
|
|
208
254
|
luv rm --dead --host box # scope either selector to one machine
|
|
@@ -217,9 +263,10 @@ registry as soon as its tmux session exits, but the clone it was working in
|
|
|
217
263
|
stays on the remote disk. Those orphans are invisible to `luv ls` and are
|
|
218
264
|
usually what is actually filling up the box.
|
|
219
265
|
|
|
220
|
-
Only folders
|
|
221
|
-
|
|
222
|
-
|
|
266
|
+
Only folders ending in `-{N}` are ever eligible — `{repo}-{machine}-{N}`, a
|
|
267
|
+
`luv -l` copy `{repo}-{machine}-{N}_2`, and pre-slug `{repo}-{N}` alike.
|
|
268
|
+
Anything else in the workspace root is left alone, and a target that doesn't
|
|
269
|
+
resolve to one is an error rather than an `rm -rf`.
|
|
223
270
|
|
|
224
271
|
## Configuration
|
|
225
272
|
|
|
@@ -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,124 @@ 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
|
-
`
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
`luv -l <PR URL>` clones into a folder of its own every time, so a second run on
|
|
349
|
+
the same PR gets `{repo}-{machine}-{number}_2`, a third `_3`. The suffix is part
|
|
350
|
+
of the number's segment, so those folders are still workspace `{number}`
|
|
351
|
+
everywhere it matters — `luv ls`, `luv rm`, `--clean`, handover. Where a number
|
|
352
|
+
has several copies, the newest wins: that is the one you just made.
|
|
353
|
+
|
|
354
|
+
Sessions are named after the workspace folder: `~/prs/myrepo-box-42` becomes
|
|
355
|
+
`luv-myrepo-box-42`. tmux forbids `.` and `:` in session names, so a repo like
|
|
356
|
+
`foo.js` becomes `luv-foo_js-box-42`.
|
|
357
|
+
|
|
358
|
+
When the workspace is already known — reopening by number, `-pr`, or `-l -r` —
|
|
359
|
+
the laptop pins the session name down before dispatching, so `tmux new-session
|
|
360
|
+
-A` doubles as "attach if it's already running". It can't compute that name,
|
|
361
|
+
though, since the slug belongs to the machine holding the folder: it takes it
|
|
362
|
+
from the session registry, or asks the host directly, and falls back to the
|
|
363
|
+
`luv-pending-<id>` rename-on-arrival path if the host doesn't answer.
|
|
364
|
+
|
|
365
|
+
When it isn't known — a brand new workspace, plain `luv -l` (whose folder the
|
|
366
|
+
remote only picks once it sees what is already there), or bare `-n`/`-r` — the
|
|
367
|
+
laptop uses a placeholder `luv-pending-<id>` and the remote renames it once the
|
|
368
|
+
clone lands.
|
|
275
369
|
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.
|
|
370
|
+
session under `luv-myrepo-box-42-2` and warns rather than failing.
|
|
277
371
|
|
|
278
372
|
Each session also carries two tmux options, which is what `luv ls` reads:
|
|
279
373
|
|
|
280
374
|
```bash
|
|
281
|
-
tmux show-options -t luv-myrepo-42 -v @luv_id
|
|
282
|
-
tmux show-options -t luv-myrepo-42 -v @luv_workspace
|
|
375
|
+
tmux show-options -t luv-myrepo-box-42 -v @luv_id
|
|
376
|
+
tmux show-options -t luv-myrepo-box-42 -v @luv_workspace
|
|
283
377
|
```
|
|
284
378
|
|
|
285
379
|
## What runs where
|
|
@@ -288,6 +382,7 @@ tmux show-options -t luv-myrepo-42 -v @luv_workspace
|
|
|
288
382
|
|---|---|---|
|
|
289
383
|
| `luv config`, `luv --init` | Always local | – |
|
|
290
384
|
| `luv ls`, `luv continue` | Local, queries remote over SSH | – |
|
|
385
|
+
| `luv handover` | Local, drives both machines over SSH | starts one on the destination |
|
|
291
386
|
| `luv <repo> [prompt]` | Remote | yes |
|
|
292
387
|
| `luv <repo> <n>`, `-pr`, `-l` | Remote | yes |
|
|
293
388
|
| `luv <repo> -n` / `-r` | Remote | yes |
|
|
@@ -304,7 +399,7 @@ invocation, `docker compose up` runs *inside* the pane:
|
|
|
304
399
|
|
|
305
400
|
- Detaching leaves the containers running.
|
|
306
401
|
- 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
|
|
402
|
+
- The compose project name is still `luv-{repo}-{machine}-{number}`, so two workspaces on
|
|
308
403
|
the same repo don't collide.
|
|
309
404
|
|
|
310
405
|
Install Docker on the remote and add your user to the `docker` group:
|
|
@@ -340,8 +435,10 @@ discards the command — so the prompt is dropped. luv prints a note when this c
|
|
|
340
435
|
happen. Type the prompt into the running agent instead.
|
|
341
436
|
|
|
342
437
|
**`luv ls` says a host is unreachable**
|
|
343
|
-
It shows
|
|
344
|
-
|
|
438
|
+
It shows that host's last known state rather than pruning; nothing is lost. A
|
|
439
|
+
configured host with no entries yet has no state to show, so it just says its
|
|
440
|
+
sessions aren't listed. When a host is gone for good, `luv ls --prune` forgets
|
|
441
|
+
its entries — and `luv config unset remote.hosts.<name>` stops the scan.
|
|
345
442
|
|
|
346
443
|
**Junk like `35;22;1M` appears at my prompt after a dropped connection**
|
|
347
444
|
|
|
@@ -356,11 +453,29 @@ restores the terminal whatever happens to the connection. If you land in this
|
|
|
356
453
|
state from something else, `reset` (or `stty sane` plus
|
|
357
454
|
`printf '\033[?1003l\033[?1006l\033[?2004l'`) clears it.
|
|
358
455
|
|
|
456
|
+
**Handover says the destination already has that folder**
|
|
457
|
+
Something with the same name is already there — either the same workspace from
|
|
458
|
+
an earlier handover, or one created on the machine that made this one. Look at
|
|
459
|
+
it first (`ssh box ls ~/prs`), then either `luv --clean` it or pass `-f` to
|
|
460
|
+
replace it.
|
|
461
|
+
|
|
462
|
+
**After a handover the agent started a fresh conversation**
|
|
463
|
+
luv prints a warning when it can't find a transcript for the workspace. The
|
|
464
|
+
usual cause is the workspace having been opened under a different path than the
|
|
465
|
+
one the agent recorded, or a Codex rollout stored somewhere other than
|
|
466
|
+
`~/.codex/sessions`. The workspace itself is unaffected — the code, branch, and
|
|
467
|
+
uncommitted work all moved.
|
|
468
|
+
|
|
469
|
+
**Handover left the session down**
|
|
470
|
+
The source folder is never deleted without `--purge`, so restart it where it
|
|
471
|
+
was: `luv <repo> <n> -r -s <source-host>`. luv prints that exact command before
|
|
472
|
+
it starts copying.
|
|
473
|
+
|
|
359
474
|
**A session is wedged**
|
|
360
475
|
|
|
361
476
|
```bash
|
|
362
477
|
ssh box tmux ls # see everything, not just luv's
|
|
363
|
-
ssh box tmux kill-session -t luv-myrepo-42
|
|
478
|
+
ssh box tmux kill-session -t luv-myrepo-box-42
|
|
364
479
|
```
|
|
365
480
|
|
|
366
481
|
**I'm already inside tmux locally**
|