workforest 0.2.3__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.
Files changed (59) hide show
  1. workforest-0.4.0/.claude/rules/architecture.md +16 -0
  2. workforest-0.4.0/.claude/rules/conventions.md +12 -0
  3. workforest-0.4.0/.claude/rules/tests.md +17 -0
  4. {workforest-0.2.3 → workforest-0.4.0}/PKG-INFO +104 -26
  5. {workforest-0.2.3 → workforest-0.4.0}/README.md +103 -25
  6. workforest-0.4.0/completions/_workforest +44 -0
  7. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/__init__.py +1 -1
  8. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/cli.py +59 -35
  9. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/commands.py +80 -28
  10. workforest-0.4.0/src/workforest/completions.py +96 -0
  11. workforest-0.4.0/src/workforest/config.py +313 -0
  12. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/errors.py +1 -1
  13. workforest-0.4.0/src/workforest/examples/config.yaml +179 -0
  14. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/gitutil.py +26 -23
  15. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/hooks.py +5 -2
  16. workforest-0.4.0/src/workforest/integrations/__init__.py +2 -0
  17. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/integrations/claude.py +19 -8
  18. workforest-0.4.0/src/workforest/launch.py +285 -0
  19. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/output.py +22 -7
  20. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/shell/completion.bash +3 -1
  21. workforest-0.4.0/src/workforest/shell/completion.zsh +67 -0
  22. workforest-0.4.0/src/workforest/shell/workforest.sh +18 -0
  23. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/tui.py +26 -13
  24. {workforest-0.2.3 → workforest-0.4.0}/tests/conftest.py +11 -6
  25. {workforest-0.2.3 → workforest-0.4.0}/tests/test_claude.py +2 -2
  26. {workforest-0.2.3 → workforest-0.4.0}/tests/test_cli.py +18 -5
  27. {workforest-0.2.3 → workforest-0.4.0}/tests/test_commands.py +77 -4
  28. {workforest-0.2.3 → workforest-0.4.0}/tests/test_config.py +160 -31
  29. {workforest-0.2.3 → workforest-0.4.0}/tests/test_gitutil.py +20 -7
  30. workforest-0.4.0/tests/test_launch.py +431 -0
  31. workforest-0.4.0/tests/test_output.py +56 -0
  32. {workforest-0.2.3 → workforest-0.4.0}/tests/test_shell.py +68 -9
  33. {workforest-0.2.3 → workforest-0.4.0}/tests/test_tui.py +22 -6
  34. workforest-0.2.3/completions/_workforest +0 -23
  35. workforest-0.2.3/src/workforest/completions.py +0 -76
  36. workforest-0.2.3/src/workforest/config.py +0 -188
  37. workforest-0.2.3/src/workforest/examples/config.yaml +0 -141
  38. workforest-0.2.3/src/workforest/integrations/__init__.py +0 -2
  39. workforest-0.2.3/src/workforest/launch.py +0 -204
  40. workforest-0.2.3/src/workforest/shell/completion.zsh +0 -38
  41. workforest-0.2.3/src/workforest/shell/workforest.sh +0 -16
  42. workforest-0.2.3/tests/test_launch.py +0 -270
  43. {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/ci.yml +0 -0
  44. {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/publish_aur.yml +0 -0
  45. {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/publish_homebrew.yml +0 -0
  46. {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/publish_pypi.yml +0 -0
  47. {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/release.yml +0 -0
  48. {workforest-0.2.3 → workforest-0.4.0}/.gitignore +0 -0
  49. {workforest-0.2.3 → workforest-0.4.0}/LICENSE +0 -0
  50. {workforest-0.2.3 → workforest-0.4.0}/Makefile +0 -0
  51. {workforest-0.2.3 → workforest-0.4.0}/packaging/AUR/PKGBUILD.template +0 -0
  52. {workforest-0.2.3 → workforest-0.4.0}/packaging/homebrew/workforest.rb.template +0 -0
  53. {workforest-0.2.3 → workforest-0.4.0}/pyproject.toml +0 -0
  54. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/__main__.py +0 -0
  55. {workforest-0.2.3 → workforest-0.4.0}/src/workforest/shellinit.py +0 -0
  56. {workforest-0.2.3 → workforest-0.4.0}/tests/__init__.py +0 -0
  57. {workforest-0.2.3 → workforest-0.4.0}/tests/test_harness.py +0 -0
  58. {workforest-0.2.3 → workforest-0.4.0}/tests/test_hooks.py +0 -0
  59. {workforest-0.2.3 → workforest-0.4.0}/uv.lock +0 -0
@@ -0,0 +1,16 @@
1
+ # Architecture invariants
2
+
3
+ - `gitutil.py` is the only module that spawns git. Consumers get typed
4
+ results; worktree data comes from `--porcelain -z` output, never from
5
+ parsing the human-readable form.
6
+ - `cli.py` is the sole stdout writer. Commands return
7
+ `ShellAction | str | None`; stdout carries the shell-wrapper cd protocol,
8
+ so nothing else may print there (hook/script stdout is diverted to stderr).
9
+ - `tui.py`: everything except the fzf subprocess is pure and unit-tested;
10
+ fzf is the only sanctioned external tool there.
11
+ - `completions.py` must never break the shell: any error yields an empty
12
+ candidate list, and output stays plain `NAME<TAB>ANNOTATION` lines.
13
+ - `integrations/claude.py` is experimental: it reads Claude Code's private
14
+ on-disk state. Session lines are rewritten by JSON parsing, never by
15
+ string substitution.
16
+ - README.md is the project reference; there is no separate design document.
@@ -0,0 +1,12 @@
1
+ # Code conventions
2
+
3
+ - Python 3.14, uv-managed. Verify changes with `make check` (ruff lint +
4
+ format check, mypy, pytest with a 90% coverage floor). Run
5
+ `uv run ruff format .` rather than hand-formatting.
6
+ - When a return value or constant bundles fields whose positions carry
7
+ meaning, use a small named dataclass — `@dataclass(slots=True,
8
+ frozen=True)` — not an anonymous tuple or nested dict. Plain dicts are for
9
+ genuine key→value lookups only (env maps, branch→remotes).
10
+ - Pre-1.0 with zero users: on breaking changes keep the clean design and
11
+ state what to re-run (e.g. re-eval shell-init). Never add
12
+ backward-compatibility shims.
@@ -0,0 +1,17 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.py"
4
+ ---
5
+
6
+ # Test conventions
7
+
8
+ - Isolation contract (see `tests/conftest.py`): no test may read or write
9
+ the real environment. The autouse `isolated_env` fixture redirects HOME,
10
+ XDG_CONFIG_HOME, and git config, and pins SHELL=/bin/sh, EDITOR, and
11
+ NO_COLOR — rely on it instead of patching these per test.
12
+ - Build repos through the `repo` / `make_repo` fixtures and the `Repo`
13
+ helper methods (`add_branch`, `add_remote`, `write_project_config`,
14
+ `make_dirty`) rather than raw git calls.
15
+ - Coverage floor is 90% (`--cov-fail-under=90` in pyproject.toml); pure
16
+ helpers are expected to be unit-tested, subprocess/terminal glue may be
17
+ `# pragma: no cover`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: workforest
3
- Version: 0.2.3
3
+ Version: 0.4.0
4
4
  Summary: Git worktree forest management: create, open, and clean up per-branch worktrees with project-defined setup hooks
5
5
  Project-URL: Homepage, https://github.com/ArkadyBuryakov/workforest
6
6
  Author-email: Arkady Buryakov <arkady@buryakov.pro>
@@ -20,6 +20,14 @@ Description-Content-Type: text/markdown
20
20
 
21
21
  # Workforest
22
22
 
23
+ ## Elevator pitch
24
+
25
+ Your repo has one working directory; your AI agents want five. Workforest
26
+ gives every feature — and every agent — its own disposable git worktree, so
27
+ parallel work on the same repo never collides.
28
+
29
+ ## About
30
+
23
31
  Git worktree forest management: one main checkout plus any number of
24
32
  disposable, per-branch worktrees in a predictable location — created, set up,
25
33
  opened, and cleaned up with one command.
@@ -36,8 +44,8 @@ opened, and cleaned up with one command.
36
44
  - **Create** a worktree for any branch (local, remote, or brand new) and have
37
45
  it set up automatically: symlinks for untracked assets (`node_modules`,
38
46
  `.env`, …) and project-defined setup scripts.
39
- - **Open** it in your editor — in the current shell, or in a new terminal
40
- window via a configurable command template.
47
+ - **Open** it in your editor — in your terminal, in a new terminal window or
48
+ multiplexer pane, or in a GUI app, all from a small shell-command config.
41
49
  - **Run** named project scripts with well-known `WF_*` environment variables.
42
50
  - **Delete** worktrees safely, or **checkout**: collapse one back into the
43
51
  main checkout.
@@ -98,27 +106,77 @@ Layered, YAML or JSON; later layers override earlier ones:
98
106
  | project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
99
107
  | project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |
100
108
 
101
- Scalars and lists replace; the `scripts`/`openers` mappings merge per key
102
- (`null` removes an entry). `workforest config` shows the merged result and
103
- where each layer came from; `workforest init` scaffolds a project file
104
- (`--local` for a personal one).
109
+ Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
110
+ merge per key (`null` removes an entry). `workforest config` shows the
111
+ merged result and where each layer came from; `workforest init` scaffolds a
112
+ project file (`--local` for a personal one). Nothing in the environment
113
+ changes the result — files and flags only.
105
114
 
106
115
  All keys, with defaults:
107
116
 
108
117
  ```yaml
109
118
  worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
110
- opener: "" # default opener; "" → $VISUAL → $EDITOR
111
- openers: {} # name -> command template, e.g. edit: "$EDITOR {target}"
112
- window_command: "" # "" → current shell; or e.g.
113
- # "kitty --title {title} --directory {worktree} $WF_COMMAND"
119
+ opener: "" # default opener: an `openers` name or a shell command;
120
+ # "" → $VISUAL → $EDITOR
121
+ openers: {} # name -> what `-o NAME` runs, and where
122
+ wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
114
123
  symlinks: [] # untracked assets linked from main into new worktrees
115
124
  setup_scripts: [] # shell snippets run in a fresh worktree
116
125
  scripts: {} # name -> snippet for `wf run NAME`
117
126
  ```
118
127
 
119
- Openers and `window_command` are command templates sharing one variable
120
- family, which the launched process (and every script) also receives as
121
- environment variables:
128
+ ### Openers
129
+
130
+ An opener is a shell command that runs with the worktree root as working
131
+ directory, either **in your terminal** (the default: the `wf` wrapper does
132
+ `cd` there and runs it) or **in the background** (`background: true`:
133
+ spawned detached, `wf` returns immediately — for GUI apps and commands that
134
+ hand off to a daemon or multiplexer). Optionally it runs **through a
135
+ wrapper**, a command that receives it as `$WF_COMMAND` and takes it
136
+ somewhere else: a new terminal window, a tmux window, a `direnv exec`. With
137
+ no config at all, `wf` runs `$VISUAL`/`$EDITOR` in your terminal.
138
+
139
+ ```yaml
140
+ opener: win # what `wf create` / `wf open` run by default
141
+
142
+ wrappers: # get the opener command as $WF_COMMAND
143
+ kitty:
144
+ command: kitty --title "$WF_TITLE" --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
145
+ background: true
146
+ tmux: # the tmux server has its own environment: WF_ENV re-creates ours
147
+ command: tmux new-window -n "$WF_TITLE" -c "$WF_WORKTREE" "export $WF_ENV; $WF_COMMAND"
148
+ background: true
149
+
150
+ openers:
151
+ edit: $EDITOR "$WF_TARGET" # in your terminal
152
+ code: # GUI app: detached
153
+ command: code "$WF_WORKTREE"
154
+ background: true
155
+ kitty: # edit's command, in a new kitty window
156
+ from: edit
157
+ wrap: kitty
158
+ tmux: # edit's command, in a new tmux window
159
+ from: edit
160
+ wrap: tmux
161
+ git: lazygit
162
+ ```
163
+
164
+ An entry is a shell command string, or a mapping with either `command` (a
165
+ shell command, never a name) or `from` (another opener's command — one
166
+ level: the target has a `command` of its own — inheriting its `background`
167
+ unless the entry sets one), plus optionally `background: true` or
168
+ `wrap: NAME`. `wrap` and `background` never sit on the same entry: the
169
+ wrapper decides where the whole thing runs, via its own `background` flag.
170
+ `-o VALUE` (and `opener:`) is an `openers` name, else a shell command;
171
+ `-w NAME` on `create`/`open` overrides the opener's wrapper (`-w ''` for
172
+ none). Wrappers are environment-specific, so they belong in the per-machine
173
+ user config — a host without your terminal emulator simply has none.
174
+ `from` and `wrap` names are checked when the config loads, so `wf config`
175
+ reports a misspelled one.
176
+
177
+ All of them are plain shell commands, run via `$SHELL -c` with one variable
178
+ family in the environment — the same family the launched process and every
179
+ script receive:
122
180
 
123
181
  | Variable | Value |
124
182
  |---|---|
@@ -129,16 +187,21 @@ environment variables:
129
187
  | `WF_BRANCH` | its branch (empty if detached) |
130
188
  | `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
131
189
  | `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
132
-
133
- In templates, `$WF_X` (like any `$ENV` variable) inserts raw text that
134
- word-splits into multiple arguments, while `{x}` — `{worktree}`, `{target}`,
135
- `{title}`, … — inserts the shell-quoted value as exactly one argument.
136
- Openers run with the worktree root as working directory; in
137
- `window_command` the resolved opener command is additionally available as
138
- `$WF_COMMAND` (spliced into argv words) or `{command}` (one argument, for
139
- `$SHELL -c` wrappers). Spawned windows shed activation state inherited from
140
- the invoking shell (Python venv, conda, nvm, rvm) so the new session starts
141
- clean instead of carrying an environment it cannot deactivate.
190
+ | `WF_ENV` | all of the above as shell-quoted `NAME=value` assignments (launch-only) |
191
+ | `WF_COMMAND` | in a wrapper: the opener command, unexpanded |
192
+
193
+ Standard shell rules apply — there is no workforest template syntax:
194
+ `"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
195
+ braces, pipes, and `&&` mean whatever your shell says they mean (a
196
+ misspelled `$WF_VAR` expands to empty, as in any shell). `$WF_COMMAND`
197
+ reaches the wrapper unexpanded, so run it through a shell of its own for its
198
+ `$WF_*` references to resolve: `$SHELL -c "$WF_COMMAND"`. When that shell
199
+ runs somewhere this environment is not inherited — a tmux server, an ssh
200
+ host — hand the family over as text: `"export $WF_ENV; $WF_COMMAND"` is
201
+ re-parsed on the far side, quoting intact. Background
202
+ processes shed activation state inherited from the invoking shell (Python
203
+ venv, conda, nvm, rvm) so a new window starts clean instead of carrying an
204
+ environment it cannot deactivate.
142
205
 
143
206
  Fully commented reference configs:
144
207
  [`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
@@ -177,8 +240,8 @@ scripts:
177
240
  ## Commands
178
241
 
179
242
  ```
180
- workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
181
- workforest open [NAME] [-o OPENER] [-p PATH]
243
+ workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
244
+ workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
182
245
  workforest list [--porcelain]
183
246
  workforest delete NAME... [--force] [--delete-branch | --keep-branch]
184
247
  workforest checkout NAME [--force]
@@ -187,8 +250,23 @@ workforest tui [MODE]
187
250
  workforest init [--local]
188
251
  workforest config [--json]
189
252
  workforest shell-init [bash|zsh]
253
+ workforest claude copy-session SESSION_ID # experimental
190
254
  ```
191
255
 
256
+ `open` (and the opener shortcut, e.g. `wf edit`) without NAME opens the
257
+ worktree you are standing in.
258
+
259
+ `create` resolves BRANCH in order: existing local branch, then a branch on
260
+ exactly one remote (checked out tracking it), then a brand-new branch.
261
+ `REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
262
+ carry the same branch name; if that local name is already taken, `create`
263
+ prompts for a different one.
264
+
265
+ `workforest claude` (shown only when `~/.claude` exists) copies a Claude
266
+ Code session from the main worktree into the current one. It is
267
+ **experimental**: it manipulates Claude Code's private on-disk state,
268
+ which is not a stable interface, so any Claude Code update may break it.
269
+
192
270
  Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error.
193
271
  Human messages go to stderr; stdout carries only machine output (`cd`
194
272
  directives for the `wf` wrapper, `--porcelain` listings, dumps).
@@ -1,5 +1,13 @@
1
1
  # Workforest
2
2
 
3
+ ## Elevator pitch
4
+
5
+ Your repo has one working directory; your AI agents want five. Workforest
6
+ gives every feature — and every agent — its own disposable git worktree, so
7
+ parallel work on the same repo never collides.
8
+
9
+ ## About
10
+
3
11
  Git worktree forest management: one main checkout plus any number of
4
12
  disposable, per-branch worktrees in a predictable location — created, set up,
5
13
  opened, and cleaned up with one command.
@@ -16,8 +24,8 @@ opened, and cleaned up with one command.
16
24
  - **Create** a worktree for any branch (local, remote, or brand new) and have
17
25
  it set up automatically: symlinks for untracked assets (`node_modules`,
18
26
  `.env`, …) and project-defined setup scripts.
19
- - **Open** it in your editor — in the current shell, or in a new terminal
20
- window via a configurable command template.
27
+ - **Open** it in your editor — in your terminal, in a new terminal window or
28
+ multiplexer pane, or in a GUI app, all from a small shell-command config.
21
29
  - **Run** named project scripts with well-known `WF_*` environment variables.
22
30
  - **Delete** worktrees safely, or **checkout**: collapse one back into the
23
31
  main checkout.
@@ -78,27 +86,77 @@ Layered, YAML or JSON; later layers override earlier ones:
78
86
  | project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
79
87
  | project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |
80
88
 
81
- Scalars and lists replace; the `scripts`/`openers` mappings merge per key
82
- (`null` removes an entry). `workforest config` shows the merged result and
83
- where each layer came from; `workforest init` scaffolds a project file
84
- (`--local` for a personal one).
89
+ Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
90
+ merge per key (`null` removes an entry). `workforest config` shows the
91
+ merged result and where each layer came from; `workforest init` scaffolds a
92
+ project file (`--local` for a personal one). Nothing in the environment
93
+ changes the result — files and flags only.
85
94
 
86
95
  All keys, with defaults:
87
96
 
88
97
  ```yaml
89
98
  worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
90
- opener: "" # default opener; "" → $VISUAL → $EDITOR
91
- openers: {} # name -> command template, e.g. edit: "$EDITOR {target}"
92
- window_command: "" # "" → current shell; or e.g.
93
- # "kitty --title {title} --directory {worktree} $WF_COMMAND"
99
+ opener: "" # default opener: an `openers` name or a shell command;
100
+ # "" → $VISUAL → $EDITOR
101
+ openers: {} # name -> what `-o NAME` runs, and where
102
+ wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
94
103
  symlinks: [] # untracked assets linked from main into new worktrees
95
104
  setup_scripts: [] # shell snippets run in a fresh worktree
96
105
  scripts: {} # name -> snippet for `wf run NAME`
97
106
  ```
98
107
 
99
- Openers and `window_command` are command templates sharing one variable
100
- family, which the launched process (and every script) also receives as
101
- environment variables:
108
+ ### Openers
109
+
110
+ An opener is a shell command that runs with the worktree root as working
111
+ directory, either **in your terminal** (the default: the `wf` wrapper does
112
+ `cd` there and runs it) or **in the background** (`background: true`:
113
+ spawned detached, `wf` returns immediately — for GUI apps and commands that
114
+ hand off to a daemon or multiplexer). Optionally it runs **through a
115
+ wrapper**, a command that receives it as `$WF_COMMAND` and takes it
116
+ somewhere else: a new terminal window, a tmux window, a `direnv exec`. With
117
+ no config at all, `wf` runs `$VISUAL`/`$EDITOR` in your terminal.
118
+
119
+ ```yaml
120
+ opener: win # what `wf create` / `wf open` run by default
121
+
122
+ wrappers: # get the opener command as $WF_COMMAND
123
+ kitty:
124
+ command: kitty --title "$WF_TITLE" --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
125
+ background: true
126
+ tmux: # the tmux server has its own environment: WF_ENV re-creates ours
127
+ command: tmux new-window -n "$WF_TITLE" -c "$WF_WORKTREE" "export $WF_ENV; $WF_COMMAND"
128
+ background: true
129
+
130
+ openers:
131
+ edit: $EDITOR "$WF_TARGET" # in your terminal
132
+ code: # GUI app: detached
133
+ command: code "$WF_WORKTREE"
134
+ background: true
135
+ kitty: # edit's command, in a new kitty window
136
+ from: edit
137
+ wrap: kitty
138
+ tmux: # edit's command, in a new tmux window
139
+ from: edit
140
+ wrap: tmux
141
+ git: lazygit
142
+ ```
143
+
144
+ An entry is a shell command string, or a mapping with either `command` (a
145
+ shell command, never a name) or `from` (another opener's command — one
146
+ level: the target has a `command` of its own — inheriting its `background`
147
+ unless the entry sets one), plus optionally `background: true` or
148
+ `wrap: NAME`. `wrap` and `background` never sit on the same entry: the
149
+ wrapper decides where the whole thing runs, via its own `background` flag.
150
+ `-o VALUE` (and `opener:`) is an `openers` name, else a shell command;
151
+ `-w NAME` on `create`/`open` overrides the opener's wrapper (`-w ''` for
152
+ none). Wrappers are environment-specific, so they belong in the per-machine
153
+ user config — a host without your terminal emulator simply has none.
154
+ `from` and `wrap` names are checked when the config loads, so `wf config`
155
+ reports a misspelled one.
156
+
157
+ All of them are plain shell commands, run via `$SHELL -c` with one variable
158
+ family in the environment — the same family the launched process and every
159
+ script receive:
102
160
 
103
161
  | Variable | Value |
104
162
  |---|---|
@@ -109,16 +167,21 @@ environment variables:
109
167
  | `WF_BRANCH` | its branch (empty if detached) |
110
168
  | `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
111
169
  | `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
112
-
113
- In templates, `$WF_X` (like any `$ENV` variable) inserts raw text that
114
- word-splits into multiple arguments, while `{x}` — `{worktree}`, `{target}`,
115
- `{title}`, … — inserts the shell-quoted value as exactly one argument.
116
- Openers run with the worktree root as working directory; in
117
- `window_command` the resolved opener command is additionally available as
118
- `$WF_COMMAND` (spliced into argv words) or `{command}` (one argument, for
119
- `$SHELL -c` wrappers). Spawned windows shed activation state inherited from
120
- the invoking shell (Python venv, conda, nvm, rvm) so the new session starts
121
- clean instead of carrying an environment it cannot deactivate.
170
+ | `WF_ENV` | all of the above as shell-quoted `NAME=value` assignments (launch-only) |
171
+ | `WF_COMMAND` | in a wrapper: the opener command, unexpanded |
172
+
173
+ Standard shell rules apply — there is no workforest template syntax:
174
+ `"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
175
+ braces, pipes, and `&&` mean whatever your shell says they mean (a
176
+ misspelled `$WF_VAR` expands to empty, as in any shell). `$WF_COMMAND`
177
+ reaches the wrapper unexpanded, so run it through a shell of its own for its
178
+ `$WF_*` references to resolve: `$SHELL -c "$WF_COMMAND"`. When that shell
179
+ runs somewhere this environment is not inherited — a tmux server, an ssh
180
+ host — hand the family over as text: `"export $WF_ENV; $WF_COMMAND"` is
181
+ re-parsed on the far side, quoting intact. Background
182
+ processes shed activation state inherited from the invoking shell (Python
183
+ venv, conda, nvm, rvm) so a new window starts clean instead of carrying an
184
+ environment it cannot deactivate.
122
185
 
123
186
  Fully commented reference configs:
124
187
  [`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
@@ -157,8 +220,8 @@ scripts:
157
220
  ## Commands
158
221
 
159
222
  ```
160
- workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
161
- workforest open [NAME] [-o OPENER] [-p PATH]
223
+ workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
224
+ workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
162
225
  workforest list [--porcelain]
163
226
  workforest delete NAME... [--force] [--delete-branch | --keep-branch]
164
227
  workforest checkout NAME [--force]
@@ -167,8 +230,23 @@ workforest tui [MODE]
167
230
  workforest init [--local]
168
231
  workforest config [--json]
169
232
  workforest shell-init [bash|zsh]
233
+ workforest claude copy-session SESSION_ID # experimental
170
234
  ```
171
235
 
236
+ `open` (and the opener shortcut, e.g. `wf edit`) without NAME opens the
237
+ worktree you are standing in.
238
+
239
+ `create` resolves BRANCH in order: existing local branch, then a branch on
240
+ exactly one remote (checked out tracking it), then a brand-new branch.
241
+ `REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
242
+ carry the same branch name; if that local name is already taken, `create`
243
+ prompts for a different one.
244
+
245
+ `workforest claude` (shown only when `~/.claude` exists) copies a Claude
246
+ Code session from the main worktree into the current one. It is
247
+ **experimental**: it manipulates Claude Code's private on-disk state,
248
+ which is not a stable interface, so any Claude Code update may break it.
249
+
172
250
  Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error.
173
251
  Human messages go to stderr; stdout carries only machine output (`cd`
174
252
  directives for the `wf` wrapper, `--porcelain` listings, dumps).
@@ -0,0 +1,44 @@
1
+ #compdef workforest wf
2
+ # zsh completion for workforest / wf — installed to zsh site-functions.
3
+ # Dynamic candidates are delegated to `workforest --complete TOPIC`.
4
+
5
+ local topic cmd line name rest kind desc
6
+ local -a items cmds
7
+ cmd="${words[2]:-}"
8
+ if (( CURRENT == 2 )); then
9
+ topic=commands
10
+ else
11
+ case "$cmd" in
12
+ create) topic=branches ;;
13
+ open|delete|checkout) topic=worktrees ;;
14
+ run) topic=scripts ;;
15
+ claude) topic=claude-sessions ;;
16
+ tui|list|init|config|shell-init) topic=none ;;
17
+ *) topic=worktrees ;;
18
+ esac
19
+ fi
20
+ if [[ "$topic" != none ]]; then
21
+ items=(${(f)"$(workforest --complete "$topic" 2>/dev/null)"})
22
+ if [[ "$topic" == commands ]]; then
23
+ # NAME<TAB>KIND<TAB>DESCRIPTION → described candidates; one group
24
+ # (kind spelled out in the description) so list order, alignment,
25
+ # and the command/opener distinction survive fzf-tab's merging.
26
+ # Cyan for openers, but only under fzf-tab (fzf renders ANSI with
27
+ # --ansi; plain complist would print the escapes literally).
28
+ local pre="" post=""
29
+ if (( ${+functions[fzf-tab-complete]} )); then
30
+ pre=$'\e[36m' post=$'\e[0m'
31
+ fi
32
+ for line in "${items[@]}"; do
33
+ name="${line%%$'\t'*}"
34
+ rest="${line#*$'\t'}"
35
+ kind="${rest%%$'\t'*}"
36
+ desc="${rest#*$'\t'}"
37
+ [[ "$kind" == opener ]] && desc="${pre}opener: ${desc}${post}"
38
+ cmds+=("${name//:/\\:}:${desc}")
39
+ done
40
+ (( ${#cmds} )) && _describe -t commands 'workforest command' cmds
41
+ else
42
+ (( ${#items} )) && compadd -a items
43
+ fi
44
+ fi
@@ -1,3 +1,3 @@
1
1
  """Workforest — git worktree forest management."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.4.0"