workforest 0.5.0__tar.gz → 0.5.2__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 (64) hide show
  1. {workforest-0.5.0 → workforest-0.5.2}/.claude/rules/architecture.md +12 -0
  2. {workforest-0.5.0 → workforest-0.5.2}/.github/workflows/ci.yml +4 -0
  3. {workforest-0.5.0 → workforest-0.5.2}/.github/workflows/publish_aur.yml +15 -5
  4. {workforest-0.5.0 → workforest-0.5.2}/.github/workflows/publish_homebrew.yml +1 -1
  5. {workforest-0.5.0 → workforest-0.5.2}/.gitignore +5 -1
  6. {workforest-0.5.0 → workforest-0.5.2}/PKG-INFO +77 -17
  7. {workforest-0.5.0 → workforest-0.5.2}/README.md +76 -16
  8. workforest-0.5.2/assets/logo-dark.svg +3 -0
  9. workforest-0.5.2/assets/logo-light.svg +3 -0
  10. workforest-0.5.2/assets/logo.svg +3 -0
  11. {workforest-0.5.0 → workforest-0.5.2}/man/workforest.1 +51 -16
  12. {workforest-0.5.0 → workforest-0.5.2}/man/workforest.5 +40 -4
  13. {workforest-0.5.0 → workforest-0.5.2}/packaging/homebrew/workforest.rb.template +10 -4
  14. {workforest-0.5.0 → workforest-0.5.2}/pyproject.toml +1 -1
  15. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/__init__.py +1 -1
  16. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/cli.py +7 -3
  17. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/commands.py +43 -11
  18. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/config.py +67 -10
  19. workforest-0.5.2/src/workforest/examples/.workforest.yaml +106 -0
  20. workforest-0.5.2/src/workforest/hooks.py +864 -0
  21. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/output.py +5 -3
  22. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/shell/completion.bash +1 -1
  23. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/shell/completion.zsh +1 -1
  24. workforest-0.5.2/src/workforest/shell/workforest.sh +25 -0
  25. workforest-0.5.2/src/workforest/templates/project.yaml +21 -0
  26. {workforest-0.5.0 → workforest-0.5.2}/tests/test_cli.py +10 -0
  27. {workforest-0.5.0 → workforest-0.5.2}/tests/test_commands.py +41 -3
  28. {workforest-0.5.0 → workforest-0.5.2}/tests/test_config.py +71 -1
  29. {workforest-0.5.0 → workforest-0.5.2}/tests/test_hooks.py +422 -0
  30. {workforest-0.5.0 → workforest-0.5.2}/tests/test_shell.py +11 -0
  31. workforest-0.5.0/src/workforest/hooks.py +0 -493
  32. workforest-0.5.0/src/workforest/shell/workforest.sh +0 -18
  33. {workforest-0.5.0 → workforest-0.5.2}/.claude/rules/conventions.md +0 -0
  34. {workforest-0.5.0 → workforest-0.5.2}/.claude/rules/tests.md +0 -0
  35. {workforest-0.5.0 → workforest-0.5.2}/.github/workflows/publish_pypi.yml +0 -0
  36. {workforest-0.5.0 → workforest-0.5.2}/.github/workflows/release.yml +0 -0
  37. {workforest-0.5.0 → workforest-0.5.2}/LICENSE +0 -0
  38. {workforest-0.5.0 → workforest-0.5.2}/Makefile +0 -0
  39. {workforest-0.5.0 → workforest-0.5.2}/completions/_workforest +0 -0
  40. {workforest-0.5.0 → workforest-0.5.2}/man/wf.1 +0 -0
  41. {workforest-0.5.0 → workforest-0.5.2}/man/wf.5 +0 -0
  42. {workforest-0.5.0 → workforest-0.5.2}/packaging/AUR/PKGBUILD.template +0 -0
  43. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/__main__.py +0 -0
  44. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/completions.py +0 -0
  45. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/errors.py +0 -0
  46. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/examples/config.yaml +0 -0
  47. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/gitutil.py +0 -0
  48. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/integrations/__init__.py +0 -0
  49. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/integrations/claude.py +0 -0
  50. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/jobs.py +0 -0
  51. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/launch.py +0 -0
  52. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/shellinit.py +0 -0
  53. {workforest-0.5.0 → workforest-0.5.2}/src/workforest/tui.py +0 -0
  54. {workforest-0.5.0 → workforest-0.5.2}/tests/__init__.py +0 -0
  55. {workforest-0.5.0 → workforest-0.5.2}/tests/conftest.py +0 -0
  56. {workforest-0.5.0 → workforest-0.5.2}/tests/test_claude.py +0 -0
  57. {workforest-0.5.0 → workforest-0.5.2}/tests/test_gitutil.py +0 -0
  58. {workforest-0.5.0 → workforest-0.5.2}/tests/test_harness.py +0 -0
  59. {workforest-0.5.0 → workforest-0.5.2}/tests/test_jobs.py +0 -0
  60. {workforest-0.5.0 → workforest-0.5.2}/tests/test_launch.py +0 -0
  61. {workforest-0.5.0 → workforest-0.5.2}/tests/test_man.py +0 -0
  62. {workforest-0.5.0 → workforest-0.5.2}/tests/test_output.py +0 -0
  63. {workforest-0.5.0 → workforest-0.5.2}/tests/test_tui.py +0 -0
  64. {workforest-0.5.0 → workforest-0.5.2}/uv.lock +0 -0
@@ -19,3 +19,15 @@
19
19
  environment variables, or exit codes updates both README.md and the
20
20
  affected page in the same change. `tests/test_man.py` catches drift from
21
21
  `cli.py`, not from prose — keep the wording in sync by hand.
22
+ - Script groups (`hooks.py`): a `bulk`/`pipeline` runs under a forked
23
+ supervisor that leads the process group and takes the tty exactly like a
24
+ command, so records, `wf stop`, `exclusive`, `cleanup`, and `-b` need no
25
+ group-specific code. Bulk members must never take the tty (only one
26
+ group can own it); they run in the supervisor's process group so a
27
+ signal to the group reaches them all, write to a pty when stderr is a
28
+ terminal (so their programs keep colors) and to a pipe otherwise, and
29
+ the supervisor relays their lines prefixed. A bulk always waits for
30
+ every member. An outcome of `-SIGINT` or exit 130 is an interruption
31
+ (warning, `wf` dies by SIGINT), never a failure. Forked-child bodies are
32
+ `# pragma: no cover`; the logic lives in tested pure helpers
33
+ (`_Prefixer`, `_pump`, `_bulk_outcome`, `_run_step`).
@@ -49,3 +49,7 @@ jobs:
49
49
  # puts that directory on $MANPATH for venv installs
50
50
  MANPATH=/tmp/venv/share/man man -w workforest wf 5 workforest wf
51
51
  /tmp/venv/bin/workforest shell-init bash | grep -q 'MANPATH=/tmp/venv/share/man'
52
+ # the init starter is package data too
53
+ git init -q /tmp/repo
54
+ (cd /tmp/repo && /tmp/venv/bin/workforest init)
55
+ test -s /tmp/repo/.workforest.yaml
@@ -3,11 +3,18 @@ name: Publish to AUR
3
3
  on:
4
4
  release:
5
5
  types: [published]
6
+ workflow_dispatch:
7
+ inputs:
8
+ version:
9
+ description: "Release version to publish (e.g. 0.2.1)"
10
+ required: true
6
11
 
7
12
  jobs:
8
13
  publish:
9
14
  runs-on: ubuntu-latest
10
15
  container: archlinux:latest
16
+ env:
17
+ RELEASE_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
11
18
  environment:
12
19
  name: aur
13
20
  url: https://aur.archlinux.org/packages/workforest
@@ -15,15 +22,18 @@ jobs:
15
22
  contents: read
16
23
  steps:
17
24
  - name: Install system dependencies
18
- run: >
19
- pacman -Syu --noconfirm base-devel git openssh pacman-contrib namcap man-db
20
- python python-yaml python-build python-installer python-wheel python-hatchling
25
+ run: |
26
+ # The archlinux image drops man pages at install time
27
+ # (NoExtract = usr/share/man/*); the smoke test below needs them.
28
+ sed -i '/^NoExtract.*usr\/share\/man/d' /etc/pacman.conf
29
+ pacman -Syu --noconfirm base-devel git openssh pacman-contrib namcap man-db \
30
+ python python-yaml python-build python-installer python-wheel python-hatchling
21
31
 
22
32
  - uses: actions/checkout@v4
23
33
 
24
34
  - name: Render PKGBUILD from template
25
35
  run: |
26
- version="${GITHUB_REF_NAME#v}"
36
+ version="${RELEASE_VERSION#v}"
27
37
  cd packaging/AUR
28
38
  sed "s/@VERSION@/$version/g" PKGBUILD.template > PKGBUILD
29
39
  # makepkg refuses to run as root
@@ -47,7 +57,7 @@ jobs:
47
57
  env:
48
58
  AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
49
59
  run: |
50
- version="${GITHUB_REF_NAME#v}"
60
+ version="${RELEASE_VERSION#v}"
51
61
  # Explicit paths only: ssh resolves ~ from the passwd entry (/root),
52
62
  # while the shell resolves it from $HOME (/github/home) — they differ
53
63
  # in container jobs. Host keys are pinned rather than keyscanned.
@@ -48,7 +48,7 @@ jobs:
48
48
  test -f "$(brew --prefix)/share/man/man1/wf.1"
49
49
  test -f "$(brew --prefix)/share/man/man5/workforest.5"
50
50
  test -f "$(brew --prefix)/share/man/man5/wf.5"
51
- # the formula relocates them, so shell-init must not touch MANPATH
51
+ # the formula moves them out of the venv, so shell-init must not touch MANPATH
52
52
  ! workforest shell-init bash | grep -q MANPATH
53
53
 
54
54
  - name: Push to tap
@@ -1,3 +1,7 @@
1
+ # IDE
2
+ .vscode
3
+ .idea
4
+
1
5
  # Python
2
6
  __pycache__/
3
7
  *.pyc
@@ -21,4 +25,4 @@ packaging/AUR/.SRCINFO
21
25
  packaging/homebrew/workforest.rb
22
26
 
23
27
  # Tooling config
24
- .workforest.yaml
28
+ /.workforest.yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: workforest
3
- Version: 0.5.0
3
+ Version: 0.5.2
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>
@@ -18,6 +18,13 @@ Requires-Python: >=3.14
18
18
  Requires-Dist: pyyaml>=6.0
19
19
  Description-Content-Type: text/markdown
20
20
 
21
+ <p align="center">
22
+ <picture>
23
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ArkadyBuryakov/workforest/HEAD/assets/logo-dark.svg">
24
+ <img src="https://raw.githubusercontent.com/ArkadyBuryakov/workforest/HEAD/assets/logo-light.svg" alt="Workforest logo" width="160">
25
+ </picture>
26
+ </p>
27
+
21
28
  # Workforest
22
29
 
23
30
  ## Elevator pitch
@@ -71,11 +78,12 @@ line to your `~/.bashrc` / `~/.zshrc`:
71
78
  eval "$(workforest shell-init)"
72
79
  ```
73
80
 
74
- This upgrades `wf` to a shell function (needed so `wf open` can change your
75
- shell's directory — a plain binary cannot), registers completions, and — for
76
- `uv tool`/`pipx` installs, whose files live in a venv — puts the man pages on
77
- `$MANPATH`. Without it everything still works, but "open in current shell"
78
- prints the `cd` command instead of performing it.
81
+ This upgrades both `workforest` and `wf` to one shell function (needed so
82
+ `open` can change your shell's directory — a plain binary cannot; either
83
+ spelling, or an alias of either, behaves the same), registers completions,
84
+ and — for `uv tool`/`pipx` installs, whose files live in a venv — puts the
85
+ man pages on `$MANPATH`. Without it everything still works, but "open in
86
+ current shell" prints the `cd` command instead of performing it.
79
87
 
80
88
  Reference: `man workforest` (commands) and `man 5 workforest` (the
81
89
  configuration files); `wf` works in place of `workforest` for both.
@@ -110,8 +118,9 @@ Layered, YAML or JSON; later layers override earlier ones:
110
118
 
111
119
  Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
112
120
  merge per key (`null` removes an entry). `workforest config` shows the
113
- merged result and where each layer came from; `workforest init` scaffolds a
114
- project file (`--local` for a personal one). Nothing in the environment
121
+ merged result and where each layer came from; `workforest init` writes a
122
+ short, fully commented starter (`--local` for a personal one) — inert until
123
+ you uncomment something. Nothing in the environment
115
124
  changes the result — files and flags only.
116
125
 
117
126
  All keys, with defaults:
@@ -124,14 +133,14 @@ openers: {} # name -> what `-o NAME` runs, and where
124
133
  wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
125
134
  symlinks: [] # untracked assets linked from main into new worktrees
126
135
  setup_scripts: [] # shell snippets run in a fresh worktree
127
- scripts: {} # name -> command for `wf run NAME` (or a mapping, see Scripts)
136
+ scripts: {} # name -> command or group for `wf run NAME` (see Scripts)
128
137
  stop_timeout: 30 # seconds a stopped script gets after SIGTERM before SIGKILL
129
138
  ```
130
139
 
131
140
  ### Openers
132
141
 
133
142
  An opener is a shell command that runs with the worktree root as working
134
- directory, either **in your terminal** (the default: the `wf` wrapper does
143
+ directory, either **in your terminal** (the default: the shell function does
135
144
  `cd` there and runs it) or **in the background** (`background: true`:
136
145
  spawned detached, `wf` returns immediately — for GUI apps and commands that
137
146
  hand off to a daemon or multiplexer). Optionally it runs **through a
@@ -215,7 +224,7 @@ installed to `/usr/share/doc/workforest/examples/` by the Arch package.
215
224
 
216
225
  `wf run NAME [ARGS...]` runs a `scripts` entry from the root of the current
217
226
  worktree, with any extra arguments shell-quoted and appended. An entry is a
218
- shell command string, or a mapping:
227
+ shell command string, a mapping, or a group of other entries (see below):
219
228
 
220
229
  ```yaml
221
230
  scripts:
@@ -236,9 +245,10 @@ terminal, so Ctrl-C and Ctrl-Z reach it directly; a signal sent to `wf`
236
245
  itself (SIGINT, SIGTERM, SIGHUP) is forwarded to the whole group. `cleanup`
237
246
  then runs — after a normal exit, a failure, or a kill — in the same
238
247
  worktree with the same `WF_*` variables. `wf run` fails with the command's
239
- status, or `128+N` when the command was killed by signal N; a command
240
- killed by SIGINT ends `wf` the way Ctrl-C would, so a shell loop around it
241
- aborts.
248
+ status, or `128+N` when the command was killed by signal N. A command
249
+ killed by SIGINT — or exiting 130, as a shell does once its child was — is
250
+ reported as interrupted, a warning rather than an error, and ends `wf` the
251
+ way Ctrl-C would, so a shell loop around it aborts.
242
252
 
243
253
  A `background` script (or `wf run -b NAME`) is detached instead: it runs
244
254
  under a supervisor of its own — the same process-group and cleanup
@@ -267,6 +277,48 @@ orphan) is stopped and cleaned up by whoever stops it when `/proc`
267
277
  confirms the pid was not recycled; elsewhere it is reported and left
268
278
  alone.
269
279
 
280
+ #### Groups
281
+
282
+ A `bulk` entry runs other entries at once; a `pipeline` entry runs them one
283
+ after another. Members are named, not written inline, so each keeps its own
284
+ `exclusive`, `cleanup`, and `stop_timeout`, and a group may name another
285
+ group (cycles are a config error):
286
+
287
+ ```yaml
288
+ scripts:
289
+ migrate: npm run db:migrate
290
+ dev:
291
+ bulk: [backend, frontend] # both at once; done when both have ended
292
+ fresh:
293
+ pipeline: [migrate, dev] # in order; the first failure ends it
294
+ background: true
295
+ ```
296
+
297
+ A group is a script like any other: `wf run -b`, `background`,
298
+ `exclusive`, and `cleanup` apply to it, and `wf stop GROUP` stops every
299
+ member, waits for all their cleanups, then runs the group's own. A group
300
+ takes no extra arguments; its `stop_timeout` defaults to the longest of
301
+ its members'. Members stay individually visible: `wf stop MEMBER` stops
302
+ that member out from under a running group, and a member already running
303
+ in the worktree fails its step.
304
+
305
+ A pipeline's steps run like consecutive `wf run`s, each with the
306
+ terminal; a `background` member is started and left running while the
307
+ pipeline moves on. The pipeline ends with the first failing step's status.
308
+
309
+ A bulk relays its members' output line by line, each line prefixed with
310
+ the member's name:
311
+
312
+ ```
313
+ backend | Attaching to db-1, api-1
314
+ frontend | VITE v5.4 ready in 312 ms
315
+ backend | api-1 | listening on :8000
316
+ ```
317
+
318
+ Members cannot read the terminal, but Ctrl-C reaches them all. A bulk
319
+ waits for every member — so `bulk: [lint, test, typecheck]` shows every
320
+ failure, not just the first — and fails if any of them did.
321
+
270
322
  ### Script environment
271
323
 
272
324
  `setup_scripts`, `scripts`, and hooks run via `$SHELL -c` with:
@@ -303,6 +355,10 @@ scripts:
303
355
  background: true
304
356
  cleanup: docker compose down
305
357
  stop_timeout: 60
358
+ dev:
359
+ bulk: [backend, frontend]
360
+ fresh:
361
+ pipeline: [migrate, dev]
306
362
  ```
307
363
 
308
364
  ## Commands
@@ -310,7 +366,7 @@ scripts:
310
366
  ```
311
367
  workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
312
368
  workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
313
- workforest list [--porcelain]
369
+ workforest list [--porcelain | --json]
314
370
  workforest delete NAME... [--force] [--delete-branch | --keep-branch]
315
371
  workforest checkout NAME [--force]
316
372
  workforest run [-b] SCRIPT [ARGS...]
@@ -338,7 +394,11 @@ which is not a stable interface, so any Claude Code update may break it.
338
394
  Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error
339
395
  · `128+N` the `run` command was killed by signal N.
340
396
  Human messages go to stderr; stdout carries only machine output (`cd`
341
- directives for the `wf` wrapper, `--porcelain` listings, dumps).
397
+ directives for the shell function, `--porcelain`/`--json` listings, dumps).
398
+ `list --json` describes the whole forest for programs — `main` (the main
399
+ checkout, in the same `name`/`branch`/`path`/`dirty` shape as each entry of
400
+ `worktrees`) and the resolved `worktrees_dir` — for editor integrations
401
+ and other programs.
342
402
 
343
403
  ## Development
344
404
 
@@ -353,7 +413,7 @@ Man pages are hand-written roff under `man/` (`workforest.1` and
353
413
  `workforest.5`, plus `wf.1`/`wf.5` links to them); `tests/test_man.py` fails when they drift from
354
414
  `cli.py`. They ship as `share/man` data in the wheel, which is how every
355
415
  package gets them: the Arch package installs the wheel to `/usr`, the
356
- Homebrew formula relocates them out of its venv, and `uv tool`/`pipx` keep
416
+ Homebrew formula moves them out of its venv, and `uv tool`/`pipx` keep
357
417
  them in the venv where `workforest shell-init` points `$MANPATH`.
358
418
 
359
419
  Packaging templates live under `packaging/` (one directory per package
@@ -1,3 +1,10 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ArkadyBuryakov/workforest/HEAD/assets/logo-dark.svg">
4
+ <img src="https://raw.githubusercontent.com/ArkadyBuryakov/workforest/HEAD/assets/logo-light.svg" alt="Workforest logo" width="160">
5
+ </picture>
6
+ </p>
7
+
1
8
  # Workforest
2
9
 
3
10
  ## Elevator pitch
@@ -51,11 +58,12 @@ line to your `~/.bashrc` / `~/.zshrc`:
51
58
  eval "$(workforest shell-init)"
52
59
  ```
53
60
 
54
- This upgrades `wf` to a shell function (needed so `wf open` can change your
55
- shell's directory — a plain binary cannot), registers completions, and — for
56
- `uv tool`/`pipx` installs, whose files live in a venv — puts the man pages on
57
- `$MANPATH`. Without it everything still works, but "open in current shell"
58
- prints the `cd` command instead of performing it.
61
+ This upgrades both `workforest` and `wf` to one shell function (needed so
62
+ `open` can change your shell's directory — a plain binary cannot; either
63
+ spelling, or an alias of either, behaves the same), registers completions,
64
+ and — for `uv tool`/`pipx` installs, whose files live in a venv — puts the
65
+ man pages on `$MANPATH`. Without it everything still works, but "open in
66
+ current shell" prints the `cd` command instead of performing it.
59
67
 
60
68
  Reference: `man workforest` (commands) and `man 5 workforest` (the
61
69
  configuration files); `wf` works in place of `workforest` for both.
@@ -90,8 +98,9 @@ Layered, YAML or JSON; later layers override earlier ones:
90
98
 
91
99
  Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
92
100
  merge per key (`null` removes an entry). `workforest config` shows the
93
- merged result and where each layer came from; `workforest init` scaffolds a
94
- project file (`--local` for a personal one). Nothing in the environment
101
+ merged result and where each layer came from; `workforest init` writes a
102
+ short, fully commented starter (`--local` for a personal one) — inert until
103
+ you uncomment something. Nothing in the environment
95
104
  changes the result — files and flags only.
96
105
 
97
106
  All keys, with defaults:
@@ -104,14 +113,14 @@ openers: {} # name -> what `-o NAME` runs, and where
104
113
  wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
105
114
  symlinks: [] # untracked assets linked from main into new worktrees
106
115
  setup_scripts: [] # shell snippets run in a fresh worktree
107
- scripts: {} # name -> command for `wf run NAME` (or a mapping, see Scripts)
116
+ scripts: {} # name -> command or group for `wf run NAME` (see Scripts)
108
117
  stop_timeout: 30 # seconds a stopped script gets after SIGTERM before SIGKILL
109
118
  ```
110
119
 
111
120
  ### Openers
112
121
 
113
122
  An opener is a shell command that runs with the worktree root as working
114
- directory, either **in your terminal** (the default: the `wf` wrapper does
123
+ directory, either **in your terminal** (the default: the shell function does
115
124
  `cd` there and runs it) or **in the background** (`background: true`:
116
125
  spawned detached, `wf` returns immediately — for GUI apps and commands that
117
126
  hand off to a daemon or multiplexer). Optionally it runs **through a
@@ -195,7 +204,7 @@ installed to `/usr/share/doc/workforest/examples/` by the Arch package.
195
204
 
196
205
  `wf run NAME [ARGS...]` runs a `scripts` entry from the root of the current
197
206
  worktree, with any extra arguments shell-quoted and appended. An entry is a
198
- shell command string, or a mapping:
207
+ shell command string, a mapping, or a group of other entries (see below):
199
208
 
200
209
  ```yaml
201
210
  scripts:
@@ -216,9 +225,10 @@ terminal, so Ctrl-C and Ctrl-Z reach it directly; a signal sent to `wf`
216
225
  itself (SIGINT, SIGTERM, SIGHUP) is forwarded to the whole group. `cleanup`
217
226
  then runs — after a normal exit, a failure, or a kill — in the same
218
227
  worktree with the same `WF_*` variables. `wf run` fails with the command's
219
- status, or `128+N` when the command was killed by signal N; a command
220
- killed by SIGINT ends `wf` the way Ctrl-C would, so a shell loop around it
221
- aborts.
228
+ status, or `128+N` when the command was killed by signal N. A command
229
+ killed by SIGINT — or exiting 130, as a shell does once its child was — is
230
+ reported as interrupted, a warning rather than an error, and ends `wf` the
231
+ way Ctrl-C would, so a shell loop around it aborts.
222
232
 
223
233
  A `background` script (or `wf run -b NAME`) is detached instead: it runs
224
234
  under a supervisor of its own — the same process-group and cleanup
@@ -247,6 +257,48 @@ orphan) is stopped and cleaned up by whoever stops it when `/proc`
247
257
  confirms the pid was not recycled; elsewhere it is reported and left
248
258
  alone.
249
259
 
260
+ #### Groups
261
+
262
+ A `bulk` entry runs other entries at once; a `pipeline` entry runs them one
263
+ after another. Members are named, not written inline, so each keeps its own
264
+ `exclusive`, `cleanup`, and `stop_timeout`, and a group may name another
265
+ group (cycles are a config error):
266
+
267
+ ```yaml
268
+ scripts:
269
+ migrate: npm run db:migrate
270
+ dev:
271
+ bulk: [backend, frontend] # both at once; done when both have ended
272
+ fresh:
273
+ pipeline: [migrate, dev] # in order; the first failure ends it
274
+ background: true
275
+ ```
276
+
277
+ A group is a script like any other: `wf run -b`, `background`,
278
+ `exclusive`, and `cleanup` apply to it, and `wf stop GROUP` stops every
279
+ member, waits for all their cleanups, then runs the group's own. A group
280
+ takes no extra arguments; its `stop_timeout` defaults to the longest of
281
+ its members'. Members stay individually visible: `wf stop MEMBER` stops
282
+ that member out from under a running group, and a member already running
283
+ in the worktree fails its step.
284
+
285
+ A pipeline's steps run like consecutive `wf run`s, each with the
286
+ terminal; a `background` member is started and left running while the
287
+ pipeline moves on. The pipeline ends with the first failing step's status.
288
+
289
+ A bulk relays its members' output line by line, each line prefixed with
290
+ the member's name:
291
+
292
+ ```
293
+ backend | Attaching to db-1, api-1
294
+ frontend | VITE v5.4 ready in 312 ms
295
+ backend | api-1 | listening on :8000
296
+ ```
297
+
298
+ Members cannot read the terminal, but Ctrl-C reaches them all. A bulk
299
+ waits for every member — so `bulk: [lint, test, typecheck]` shows every
300
+ failure, not just the first — and fails if any of them did.
301
+
250
302
  ### Script environment
251
303
 
252
304
  `setup_scripts`, `scripts`, and hooks run via `$SHELL -c` with:
@@ -283,6 +335,10 @@ scripts:
283
335
  background: true
284
336
  cleanup: docker compose down
285
337
  stop_timeout: 60
338
+ dev:
339
+ bulk: [backend, frontend]
340
+ fresh:
341
+ pipeline: [migrate, dev]
286
342
  ```
287
343
 
288
344
  ## Commands
@@ -290,7 +346,7 @@ scripts:
290
346
  ```
291
347
  workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
292
348
  workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
293
- workforest list [--porcelain]
349
+ workforest list [--porcelain | --json]
294
350
  workforest delete NAME... [--force] [--delete-branch | --keep-branch]
295
351
  workforest checkout NAME [--force]
296
352
  workforest run [-b] SCRIPT [ARGS...]
@@ -318,7 +374,11 @@ which is not a stable interface, so any Claude Code update may break it.
318
374
  Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error
319
375
  · `128+N` the `run` command was killed by signal N.
320
376
  Human messages go to stderr; stdout carries only machine output (`cd`
321
- directives for the `wf` wrapper, `--porcelain` listings, dumps).
377
+ directives for the shell function, `--porcelain`/`--json` listings, dumps).
378
+ `list --json` describes the whole forest for programs — `main` (the main
379
+ checkout, in the same `name`/`branch`/`path`/`dirty` shape as each entry of
380
+ `worktrees`) and the resolved `worktrees_dir` — for editor integrations
381
+ and other programs.
322
382
 
323
383
  ## Development
324
384
 
@@ -333,7 +393,7 @@ Man pages are hand-written roff under `man/` (`workforest.1` and
333
393
  `workforest.5`, plus `wf.1`/`wf.5` links to them); `tests/test_man.py` fails when they drift from
334
394
  `cli.py`. They ship as `share/man` data in the wheel, which is how every
335
395
  package gets them: the Arch package installs the wheel to `/usr`, the
336
- Homebrew formula relocates them out of its venv, and `uv tool`/`pipx` keep
396
+ Homebrew formula moves them out of its venv, and `uv tool`/`pipx` keep
337
397
  them in the venv where `workforest shell-init` points `$MANPATH`.
338
398
 
339
399
  Packaging templates live under `packaging/` (one directory per package
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="26.677 49.555 48.277 47.922">
2
+ <path fill="#e6edf3" d="m 40.35,71.984 -10.673,17.25 14.26,0.002 -0.874,5.241 8.619,-0.001 -1.192,-5.47 6.692,0.001 -0.611,3.667 h 6.015 l -0.612,-3.667 9.98,-0.002 -7.47,-12.071 2.501,-0.041 -7.398,-13.555 -7.416,13.555 2.501,0.041 L 47.831,87.978 53.253156,77.719406 50.83,77.658 55.434,68.012 47.373,52.555 36.776,71.926 Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="26.677 49.555 48.277 47.922">
2
+ <path fill="#1f2328" d="m 40.35,71.984 -10.673,17.25 14.26,0.002 -0.874,5.241 8.619,-0.001 -1.192,-5.47 6.692,0.001 -0.611,3.667 h 6.015 l -0.612,-3.667 9.98,-0.002 -7.47,-12.071 2.501,-0.041 -7.398,-13.555 -7.416,13.555 2.501,0.041 L 47.831,87.978 53.253156,77.719406 50.83,77.658 55.434,68.012 47.373,52.555 36.776,71.926 Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="26.677 49.555 48.277 47.922">
2
+ <path fill="currentColor" d="m 40.35,71.984 -10.673,17.25 14.26,0.002 -0.874,5.241 8.619,-0.001 -1.192,-5.47 6.692,0.001 -0.611,3.667 h 6.015 l -0.612,-3.667 9.98,-0.002 -7.47,-12.071 2.501,-0.041 -7.398,-13.555 -7.416,13.555 2.501,0.041 L 47.831,87.978 53.253156,77.719406 50.83,77.658 55.434,68.012 47.373,52.555 36.776,71.926 Z"/>
3
+ </svg>
@@ -31,7 +31,7 @@ workforest, wf \- git worktree forest management
31
31
  .IR PATH ]
32
32
  .YS
33
33
  .SY "workforest list"
34
- .RB [ \-\-porcelain ]
34
+ .RB [ \-\-porcelain " | " \-\-json ]
35
35
  .YS
36
36
  .SY "workforest delete"
37
37
  .IR NAME .\|.\|.
@@ -106,7 +106,7 @@ After
106
106
  .B shell-init
107
107
  (see
108
108
  .BR "SHELL INTEGRATION" )
109
- it is a shell function that can change the calling shell's directory.
109
+ both are one shell function that can change the calling shell's directory.
110
110
  .SH COMMANDS
111
111
  .SS create [\fIBRANCH\fR]
112
112
  Create (or reuse) a worktree for
@@ -188,6 +188,23 @@ and
188
188
  (dirty) or
189
189
  .B 0
190
190
  (clean), separated by tabs.
191
+ .TP
192
+ .B \-\-json
193
+ The whole forest as a JSON object, for editor integrations and other
194
+ programs:
195
+ .B main
196
+ (the main checkout) and each entry of
197
+ .B worktrees
198
+ carry
199
+ .BR name ,
200
+ .B branch
201
+ (null when detached),
202
+ .BR path ,
203
+ and
204
+ .B dirty
205
+ (true/false);
206
+ .B worktrees_dir
207
+ is the resolved worktrees directory.
191
208
  .SS delete \fINAME\fR...
192
209
  Delete one or more worktrees.
193
210
  Every name is resolved before anything is removed, so a typo fails the
@@ -216,7 +233,14 @@ Skip the dirty-worktree confirmation.
216
233
  Run the named script from the merged configuration's
217
234
  .B scripts
218
235
  map, from the root of the current worktree.
219
- Extra arguments are shell-quoted and appended to the script command.
236
+ Extra arguments are shell-quoted and appended to the script command (a
237
+ group \(em a
238
+ .B bulk
239
+ or
240
+ .B pipeline
241
+ entry, see
242
+ .BR workforest (5)
243
+ \(em takes none).
220
244
  See
221
245
  .B ENVIRONMENT
222
246
  for the variables scripts receive.
@@ -239,10 +263,17 @@ while it is already running in this worktree (see
239
263
  .BR stop ).
240
264
  The exit status is the command's, or 128+\fIN\fR when it was killed by
241
265
  signal
242
- .IR N ;
243
- a command killed by SIGINT ends
266
+ .IR N .
267
+ A command killed by SIGINT \(em or exiting 130, as a shell does once its
268
+ child was \(em is reported as interrupted, a warning rather than an
269
+ error, and ends
244
270
  .B workforest
245
271
  by SIGINT as well, so a shell loop around it aborts.
272
+ A group runs under a supervisor that takes the command's place in all of
273
+ the above: a pipeline runs its members in order and ends with the first
274
+ failing step's status; a bulk runs them at once, relays their output with
275
+ the member's name as a line prefix, and ends with the first failing
276
+ member's status once all have ended.
246
277
  .TP
247
278
  .BR \-b ", " \-\-background
248
279
  Detach instead: the command runs under a supervisor of its own with its
@@ -265,6 +296,9 @@ foreground or background, started from any terminal \(em with SIGTERM,
265
296
  then SIGKILL after
266
297
  .B stop_timeout
267
298
  seconds, and wait for its cleanup to finish.
299
+ For a group this stops every member and waits for all of their cleanups,
300
+ then the group's own; a member can also be stopped by name out from
301
+ under a running group.
268
302
  Fails when nothing is running.
269
303
  .TP
270
304
  .B \-\-all
@@ -314,9 +348,12 @@ The
314
348
  .B delete
315
349
  tab stays open after each deletion for bulk cleanup.
316
350
  .SS init
317
- Scaffold a fully commented
351
+ Write a short, fully commented
318
352
  .B .workforest.yaml
319
- project configuration in the main checkout.
353
+ starter in the main checkout. Every key is commented out, so the file
354
+ changes nothing until edited; see
355
+ .BR workforest (5)
356
+ for the reference.
320
357
  .TP
321
358
  .B \-\-local
322
359
  Place it in
@@ -359,15 +396,14 @@ A subprocess cannot change its parent shell's directory, so add to your
359
396
  eval "$(workforest shell-init)"
360
397
  .EE
361
398
  .PP
362
- This defines
363
- .B wf
364
- as a shell function that runs
365
- .B workforest
366
- and evaluates the directive it prints (a
399
+ This defines both
400
+ .BR workforest " and " wf
401
+ as one shell function that runs the workforest binary and evaluates the
402
+ directive it prints (a
367
403
  .B cd
368
- into the worktree, followed by the opener command); registers tab
369
- completion for both
370
- .BR wf " and " workforest ;
404
+ into the worktree, followed by the opener command), so either spelling
405
+ \(em or an alias of either \(em behaves the same; registers tab
406
+ completion for both;
371
407
  and, when workforest was installed into a Python environment whose
372
408
  manual pages are not on the default search path (a
373
409
  .B uv tool
@@ -499,7 +535,6 @@ command was killed by signal
499
535
  .PP
500
536
  Human-readable messages go to stderr.
501
537
  Stdout carries only machine output: directives for the
502
- .B wf
503
538
  shell function, listings, and configuration dumps.
504
539
  .SH EXAMPLES
505
540
  .TP