treebox 0.3.0__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 (64) hide show
  1. treebox-0.4.0/.github/workflows/autofix.yml +69 -0
  2. {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/ci.yml +3 -0
  3. treebox-0.4.0/.pre-commit-config.yaml +25 -0
  4. {treebox-0.3.0 → treebox-0.4.0}/CLAUDE.md +4 -2
  5. treebox-0.4.0/CONTRIBUTING.md +52 -0
  6. {treebox-0.3.0 → treebox-0.4.0}/PKG-INFO +47 -9
  7. {treebox-0.3.0 → treebox-0.4.0}/README.md +44 -8
  8. {treebox-0.3.0 → treebox-0.4.0}/docs/configuration.md +45 -3
  9. {treebox-0.3.0 → treebox-0.4.0}/docs/how-it-works.md +4 -2
  10. {treebox-0.3.0 → treebox-0.4.0}/docs/index.md +56 -3
  11. {treebox-0.3.0 → treebox-0.4.0}/docs/install.md +2 -1
  12. {treebox-0.3.0 → treebox-0.4.0}/docs/javascripts/treebox.js +20 -13
  13. {treebox-0.3.0 → treebox-0.4.0}/docs/stylesheets/extra.css +59 -8
  14. {treebox-0.3.0 → treebox-0.4.0}/docs/usage.md +19 -8
  15. {treebox-0.3.0 → treebox-0.4.0}/pyproject.toml +4 -2
  16. {treebox-0.3.0 → treebox-0.4.0}/scripts/validate.sh +1 -0
  17. {treebox-0.3.0 → treebox-0.4.0}/skills/treebox/SKILL.md +3 -3
  18. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets.py +20 -2
  19. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/cli.py +111 -25
  20. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/config.py +10 -2
  21. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/git.py +10 -0
  22. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/provision.py +128 -26
  23. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/base.py +6 -0
  24. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/docker.py +65 -12
  25. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/host.py +17 -4
  26. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/state.py +8 -0
  27. {treebox-0.3.0 → treebox-0.4.0}/tests/test_integration.py +373 -0
  28. {treebox-0.3.0 → treebox-0.4.0}/tests/test_units.py +235 -11
  29. {treebox-0.3.0 → treebox-0.4.0}/uv.lock +93 -0
  30. treebox-0.3.0/CONTRIBUTING.md +0 -31
  31. {treebox-0.3.0 → treebox-0.4.0}/.agents/skills/no-mistakes/SKILL.md +0 -0
  32. {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/claude.yml +0 -0
  33. {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/docs.yml +0 -0
  34. {treebox-0.3.0 → treebox-0.4.0}/.github/workflows/release.yml +0 -0
  35. {treebox-0.3.0 → treebox-0.4.0}/.gitignore +0 -0
  36. {treebox-0.3.0 → treebox-0.4.0}/AGENTS.md +0 -0
  37. {treebox-0.3.0 → treebox-0.4.0}/LICENSE +0 -0
  38. {treebox-0.3.0 → treebox-0.4.0}/ROADMAP.md +0 -0
  39. {treebox-0.3.0 → treebox-0.4.0}/assets/treebox-logo.png +0 -0
  40. {treebox-0.3.0 → treebox-0.4.0}/docs/agents.md +0 -0
  41. {treebox-0.3.0 → treebox-0.4.0}/docs/assets/treebox-logo.png +0 -0
  42. {treebox-0.3.0 → treebox-0.4.0}/hooks/copy_page.py +0 -0
  43. {treebox-0.3.0 → treebox-0.4.0}/install.sh +0 -0
  44. {treebox-0.3.0 → treebox-0.4.0}/mkdocs.yml +0 -0
  45. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/__init__.py +0 -0
  46. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/Dockerfile +0 -0
  47. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/allowed-domains.sh +0 -0
  48. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/container.json +0 -0
  49. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/firewall.json +0 -0
  50. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/init-firewall.sh +0 -0
  51. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/container/post-create.sh +0 -0
  52. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/assets/pre-push +0 -0
  53. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/ecosystems.py +0 -0
  54. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/forge.py +0 -0
  55. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/locking.py +0 -0
  56. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/models.py +0 -0
  57. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/names.py +0 -0
  58. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/output.py +0 -0
  59. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/py.typed +0 -0
  60. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/resolve.py +0 -0
  61. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/runners/__init__.py +0 -0
  62. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/status.py +0 -0
  63. {treebox-0.3.0 → treebox-0.4.0}/src/treebox/system.py +0 -0
  64. {treebox-0.3.0 → treebox-0.4.0}/tests/conftest.py +0 -0
@@ -0,0 +1,69 @@
1
+ name: Autofix
2
+
3
+ # Applies ruff's auto-fixes (`ruff check --fix` + `ruff format`) to pull requests
4
+ # and commits the result back to the PR branch. This is the CI counterpart to the
5
+ # pre-commit hooks: if a change lands unformatted (hooks skipped), the branch is
6
+ # fixed in place instead of just failing the CI gate. Skips fork PRs, where the
7
+ # workflow token cannot push, and its own commits, to avoid loops.
8
+ #
9
+ # Gotcha: pushes made with the default GITHUB_TOKEN (from actions/checkout) do
10
+ # NOT trigger new workflow runs. So the auto-fixed commit this job pushes will
11
+ # not re-run ci.yml. Two implications:
12
+ # - If branch protection REQUIRES the CI status checks, the auto-fixed SHA will
13
+ # have no reported checks and the PR will stall. The remedy is to check out
14
+ # and push using a PAT or GitHub App token stored as a repo secret (e.g.
15
+ # AUTOFIX_TOKEN) instead of GITHUB_TOKEN, so the push re-triggers CI.
16
+ # - To close the "merged with a commit CI never validated" hole regardless,
17
+ # this job re-runs the ruff + mypy checks on the fixed tree below and fails
18
+ # rather than pushing if they don't pass, so the exact pushed commit is
19
+ # validated within this run.
20
+
21
+ on:
22
+ pull_request:
23
+
24
+ concurrency:
25
+ group: autofix-${{ github.ref }}
26
+ cancel-in-progress: true
27
+
28
+ permissions:
29
+ contents: write
30
+
31
+ jobs:
32
+ autofix:
33
+ name: ruff --fix
34
+ runs-on: ubuntu-latest
35
+ if: >-
36
+ github.event.pull_request.head.repo.full_name == github.repository &&
37
+ github.actor != 'github-actions[bot]'
38
+ steps:
39
+ - uses: actions/checkout@v7
40
+ with:
41
+ ref: ${{ github.head_ref }}
42
+
43
+ - name: Install uv
44
+ uses: astral-sh/setup-uv@v8.2.0
45
+ with:
46
+ enable-cache: true
47
+
48
+ - name: ruff check --fix
49
+ run: uv run --extra dev ruff check --fix src tests
50
+
51
+ - name: ruff format
52
+ run: uv run --extra dev ruff format src tests
53
+
54
+ - name: Validate fixed tree
55
+ run: |
56
+ uv run --extra dev ruff check src tests
57
+ uv run --extra dev ruff format --check src tests
58
+ uv run --extra dev mypy
59
+
60
+ - name: Commit fixes
61
+ run: |
62
+ if git diff --quiet; then
63
+ echo "No formatting or lint fixes needed."
64
+ exit 0
65
+ fi
66
+ git config user.name "github-actions[bot]"
67
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
68
+ git commit -am "style: apply ruff --fix and format"
69
+ git push
@@ -29,6 +29,9 @@ jobs:
29
29
  - name: Lint
30
30
  run: uv run -p ${{ matrix.python-version }} --extra dev ruff check src tests
31
31
 
32
+ - name: Format
33
+ run: uv run -p ${{ matrix.python-version }} --extra dev ruff format --check src tests
34
+
32
35
  - name: Type check
33
36
  run: uv run -p ${{ matrix.python-version }} --extra dev mypy
34
37
 
@@ -0,0 +1,25 @@
1
+ # Pre-commit hooks: keep every commit lint-clean, formatted, and type-checked.
2
+ # Install once with `uv run --extra dev pre-commit install` (or `pre-commit install`).
3
+ # Run against everything with `uv run --extra dev pre-commit run --all-files`.
4
+ #
5
+ # Keep the ruff rev in sync with the `ruff>=…` pin in pyproject.toml's [dev] extra.
6
+ repos:
7
+ - repo: https://github.com/astral-sh/ruff-pre-commit
8
+ rev: v0.15.20
9
+ hooks:
10
+ # Lint and auto-fix (import sorting, pyupgrade, simplify, …), then format.
11
+ - id: ruff-check
12
+ args: [--fix]
13
+ - id: ruff-format
14
+
15
+ # mypy runs from the project's own dev environment so it sees typer/questionary
16
+ # stubs and the [tool.mypy] config (files = ["src"]). pass_filenames is off
17
+ # because that config already selects the files to check.
18
+ - repo: local
19
+ hooks:
20
+ - id: mypy
21
+ name: mypy
22
+ entry: uv run --extra dev mypy
23
+ language: system
24
+ types: [python]
25
+ pass_filenames: false
@@ -19,9 +19,11 @@ template it provisions is separately pinned to CPython 3.14.6).
19
19
  uv run treebox ... # run the CLI from the working tree
20
20
  uv run --extra dev python -m pytest # full unit + integration suite
21
21
  uv run --extra dev python -m pytest tests/test_units.py::test_name # single test (or -k <pattern>)
22
- ruff check src tests # lint
22
+ uv run --extra dev ruff check src tests # lint
23
+ uv run --extra dev ruff format --check src tests # format check (drop --check to apply)
23
24
  uv run --extra dev mypy # type check (config in pyproject.toml)
24
- ./scripts/validate.sh # lint + tests + live host-runner smoke (real uv, throwaway local remote)
25
+ uv run --extra dev pre-commit install # install lint/format/type hooks (see CONTRIBUTING.md)
26
+ ./scripts/validate.sh # lint + format + tests + live host-runner smoke (real uv, throwaway local remote)
25
27
  uv pip install -e ".[dev]" # editable dev environment
26
28
  uv run --extra docs mkdocs serve # docs site (docs/ + mkdocs.yml), live-reloading
27
29
  uv run --extra docs mkdocs build --strict # build docs to site/ (gitignored)
@@ -0,0 +1,52 @@
1
+ # Contributing
2
+
3
+ treebox is a small, one-maintainer project. Contributions are welcome, but the scope is intentionally modest.
4
+
5
+ ## Good contributions
6
+
7
+ - Bug fixes with a clear reproduction.
8
+ - Documentation fixes that make install, usage, or runner behavior clearer.
9
+ - Small compatibility fixes that preserve the existing CLI and scripting behavior.
10
+
11
+ For larger feature ideas, open an issue first so we can decide whether they fit the project.
12
+
13
+ ## Local checks
14
+
15
+ Install the pre-commit hooks once so every commit is auto-formatted, lint-fixed,
16
+ and type-checked (`ruff format`, `ruff check --fix`, and `mypy`):
17
+
18
+ ```bash
19
+ uv run --extra dev pre-commit install
20
+ ```
21
+
22
+ Note: `pre-commit install` has no effect inside a treebox worktree, because
23
+ treebox sets `core.hooksPath` for its own pre-push guard, so git ignores the
24
+ hook pre-commit writes to `.git/hooks`. Run the hooks manually there with
25
+ `uv run --extra dev pre-commit run --all-files`.
26
+
27
+ If the hooks are skipped, the Autofix CI workflow applies the same `ruff --fix`
28
+ and `ruff format` to same-repo PRs and pushes the result back. Note that pushes
29
+ made with the default `GITHUB_TOKEN` do not re-trigger CI, so if this repo ever
30
+ requires the CI status checks under branch protection, the auto-fixed commit
31
+ will have no reported checks and the PR will stall; the remedy is to push from
32
+ the workflow using a PAT or GitHub App token stored as a repo secret (e.g.
33
+ `AUTOFIX_TOKEN`) instead of `GITHUB_TOKEN`.
34
+
35
+ For code changes, run the relevant checks before opening a PR:
36
+
37
+ ```bash
38
+ uv run --extra dev python -m pytest
39
+ uv run --extra dev ruff check src tests
40
+ uv run --extra dev ruff format --check src tests
41
+ uv run --extra dev mypy
42
+ ```
43
+
44
+ For docs-only changes, this is enough:
45
+
46
+ ```bash
47
+ uv run --extra docs mkdocs build --strict
48
+ ```
49
+
50
+ ## Boundaries
51
+
52
+ Please preserve the basics: fresh refs by default, no trust in target-repo sandbox config, stable CLI output for scripts, and subscription-based agent auth.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: treebox
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Isolated, ready-to-run git worktrees for AI coding agents — host-native or docker-sandboxed.
5
5
  Project-URL: Homepage, https://github.com/Seth-Peters/treebox
6
6
  Project-URL: Documentation, https://seth-peters.github.io/treebox/
@@ -23,9 +23,11 @@ Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Topic :: Software Development :: Version Control :: Git
24
24
  Requires-Python: >=3.11
25
25
  Requires-Dist: questionary<3,>=2
26
+ Requires-Dist: rich<16,>=13
26
27
  Requires-Dist: typer<1.0,>=0.12
27
28
  Provides-Extra: dev
28
29
  Requires-Dist: mypy>=1.14; extra == 'dev'
30
+ Requires-Dist: pre-commit>=4; extra == 'dev'
29
31
  Requires-Dist: pytest-cov>=6; extra == 'dev'
30
32
  Requires-Dist: pytest>=8; extra == 'dev'
31
33
  Requires-Dist: ruff>=0.15; extra == 'dev'
@@ -149,7 +151,7 @@ the branch starts as a `treebox/<name>` placeholder that a per-worktree
149
151
  pre-push guard keeps **un-pushable** — rename it conventionally
150
152
  (`git branch -m feature/user-auth`, `fix/login-race`, `chore/bump-deps`, …)
151
153
  when the work has a shape, then push. So a machine-generated name can never
152
- become a PR title. `-b` is the one path that skips the placeholder: it checks out an
154
+ become a PR title. `--checkout` is the one path that skips the placeholder: it checks out an
153
155
  existing branch exactly.
154
156
 
155
157
  `--base` takes any branch, not just `main` — branch off `dev`, or stack a new
@@ -157,10 +159,11 @@ worktree on top of an existing PR's branch, even while that branch is checked
157
159
  out in another worktree. It resolves as the freshly fetched `origin/<base>`,
158
160
  so push the base first if its latest commits only exist locally.
159
161
 
160
- **Enter.** Come back to an existing worktree, picking the agent per entry.
161
- The ref is the name, the *current* branch (renames are followed live), or a
162
- unique substring of either. Dependencies re-sync only if the lockfile changed
163
- since last time:
162
+ **Enter.** Come back to an existing worktree. By default it reuses the harness
163
+ the worktree was created with; an explicit `--harness` overrides it for that
164
+ session only, without changing what's recorded on disk. The ref is the name,
165
+ the *current* branch (renames are followed live), or a unique substring of
166
+ either. Dependencies re-sync only if the lockfile changed since last time:
164
167
 
165
168
  ```bash
166
169
  treebox enter fix-auth --harness claude
@@ -207,8 +210,9 @@ conflict). Full reference in the
207
210
  hooks are ignored.
208
211
  - **Credentials go in as scoped copies.** Only the agents' login files are
209
212
  copied into a throwaway per-worktree dir — never the live `~/.claude` /
210
- `~/.codex` — and treebox uses your subscription login, never
211
- `ANTHROPIC_API_KEY`.
213
+ `~/.codex` — refreshed on every entry so a host logout or a fresh login
214
+ reaches the sandbox next time, and treebox uses your subscription login,
215
+ never `ANTHROPIC_API_KEY`.
212
216
 
213
217
  More in [how it works](https://seth-peters.github.io/treebox/how-it-works/).
214
218
 
@@ -227,13 +231,47 @@ All keys, shared-cache overrides, setup hooks, and sandbox templates are
227
231
  covered in the
228
232
  [configuration guide](https://seth-peters.github.io/treebox/configuration/).
229
233
 
234
+ ## Customizing isolation
235
+
236
+ `docker` isolation is defined by an **operator-owned template** — a
237
+ `Dockerfile` + `container.json` you copy out and edit, kept beside the worktree
238
+ so a boxed agent can't touch its own cage. The shipped image bundles Node 22,
239
+ uv, `gh`, ripgrep, and the agent CLIs, so a Node project often needs no image
240
+ change. For a genuinely separate, non-Python box, copy the template and edit
241
+ two things — global tooling in the `Dockerfile`, and `postCreate` to install
242
+ your repo's deps (in docker, setup runs `postCreate`, not the host-mode
243
+ ecosystem auto-detect):
244
+
245
+ ```bash
246
+ cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.assets.__file__).parent)')/container" \
247
+ ~/.config/treebox/templates/node
248
+ ```
249
+
250
+ ```dockerfile
251
+ # ~/.config/treebox/templates/node/Dockerfile — Node 22 is already baked in
252
+ USER root
253
+ RUN npm install -g pnpm@9 typescript tsx
254
+ USER ${USERNAME}
255
+ ```
256
+
257
+ ```json
258
+ // ~/.config/treebox/templates/node/container.json
259
+ "postCreate": "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi"
260
+ ```
261
+
262
+ Then select it per run (`--isolation docker --template node`) or set
263
+ `template = "node"` in your config — a `node` box and a `python` box coexist,
264
+ one per stack. Full walkthrough in the
265
+ [configuration guide](https://seth-peters.github.io/treebox/configuration/#worked-example-a-node-sandbox-not-python).
266
+
230
267
  ## Development
231
268
 
232
269
  ```bash
233
270
  git clone https://github.com/Seth-Peters/treebox && cd treebox
234
271
  uv run treebox ... # run the CLI from the working tree
272
+ uv run --extra dev pre-commit install # lint/format/type hooks (see CONTRIBUTING.md)
235
273
  uv run --extra dev python -m pytest # unit + integration suite
236
- ./scripts/validate.sh # lint + tests + live host-runner smoke
274
+ ./scripts/validate.sh # lint + format + tests + live host-runner smoke
237
275
  ```
238
276
 
239
277
  ## Contributing
@@ -114,7 +114,7 @@ the branch starts as a `treebox/<name>` placeholder that a per-worktree
114
114
  pre-push guard keeps **un-pushable** — rename it conventionally
115
115
  (`git branch -m feature/user-auth`, `fix/login-race`, `chore/bump-deps`, …)
116
116
  when the work has a shape, then push. So a machine-generated name can never
117
- become a PR title. `-b` is the one path that skips the placeholder: it checks out an
117
+ become a PR title. `--checkout` is the one path that skips the placeholder: it checks out an
118
118
  existing branch exactly.
119
119
 
120
120
  `--base` takes any branch, not just `main` — branch off `dev`, or stack a new
@@ -122,10 +122,11 @@ worktree on top of an existing PR's branch, even while that branch is checked
122
122
  out in another worktree. It resolves as the freshly fetched `origin/<base>`,
123
123
  so push the base first if its latest commits only exist locally.
124
124
 
125
- **Enter.** Come back to an existing worktree, picking the agent per entry.
126
- The ref is the name, the *current* branch (renames are followed live), or a
127
- unique substring of either. Dependencies re-sync only if the lockfile changed
128
- since last time:
125
+ **Enter.** Come back to an existing worktree. By default it reuses the harness
126
+ the worktree was created with; an explicit `--harness` overrides it for that
127
+ session only, without changing what's recorded on disk. The ref is the name,
128
+ the *current* branch (renames are followed live), or a unique substring of
129
+ either. Dependencies re-sync only if the lockfile changed since last time:
129
130
 
130
131
  ```bash
131
132
  treebox enter fix-auth --harness claude
@@ -172,8 +173,9 @@ conflict). Full reference in the
172
173
  hooks are ignored.
173
174
  - **Credentials go in as scoped copies.** Only the agents' login files are
174
175
  copied into a throwaway per-worktree dir — never the live `~/.claude` /
175
- `~/.codex` — and treebox uses your subscription login, never
176
- `ANTHROPIC_API_KEY`.
176
+ `~/.codex` — refreshed on every entry so a host logout or a fresh login
177
+ reaches the sandbox next time, and treebox uses your subscription login,
178
+ never `ANTHROPIC_API_KEY`.
177
179
 
178
180
  More in [how it works](https://seth-peters.github.io/treebox/how-it-works/).
179
181
 
@@ -192,13 +194,47 @@ All keys, shared-cache overrides, setup hooks, and sandbox templates are
192
194
  covered in the
193
195
  [configuration guide](https://seth-peters.github.io/treebox/configuration/).
194
196
 
197
+ ## Customizing isolation
198
+
199
+ `docker` isolation is defined by an **operator-owned template** — a
200
+ `Dockerfile` + `container.json` you copy out and edit, kept beside the worktree
201
+ so a boxed agent can't touch its own cage. The shipped image bundles Node 22,
202
+ uv, `gh`, ripgrep, and the agent CLIs, so a Node project often needs no image
203
+ change. For a genuinely separate, non-Python box, copy the template and edit
204
+ two things — global tooling in the `Dockerfile`, and `postCreate` to install
205
+ your repo's deps (in docker, setup runs `postCreate`, not the host-mode
206
+ ecosystem auto-detect):
207
+
208
+ ```bash
209
+ cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.assets.__file__).parent)')/container" \
210
+ ~/.config/treebox/templates/node
211
+ ```
212
+
213
+ ```dockerfile
214
+ # ~/.config/treebox/templates/node/Dockerfile — Node 22 is already baked in
215
+ USER root
216
+ RUN npm install -g pnpm@9 typescript tsx
217
+ USER ${USERNAME}
218
+ ```
219
+
220
+ ```json
221
+ // ~/.config/treebox/templates/node/container.json
222
+ "postCreate": "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi"
223
+ ```
224
+
225
+ Then select it per run (`--isolation docker --template node`) or set
226
+ `template = "node"` in your config — a `node` box and a `python` box coexist,
227
+ one per stack. Full walkthrough in the
228
+ [configuration guide](https://seth-peters.github.io/treebox/configuration/#worked-example-a-node-sandbox-not-python).
229
+
195
230
  ## Development
196
231
 
197
232
  ```bash
198
233
  git clone https://github.com/Seth-Peters/treebox && cd treebox
199
234
  uv run treebox ... # run the CLI from the working tree
235
+ uv run --extra dev pre-commit install # lint/format/type hooks (see CONTRIBUTING.md)
200
236
  uv run --extra dev python -m pytest # unit + integration suite
201
- ./scripts/validate.sh # lint + tests + live host-runner smoke
237
+ ./scripts/validate.sh # lint + format + tests + live host-runner smoke
202
238
  ```
203
239
 
204
240
  ## Contributing
@@ -40,7 +40,7 @@ command-line flag > config.toml > built-in default
40
40
  | Key | Default | What it controls |
41
41
  | ---------- | -------------------- | --------------------------------------------------------- |
42
42
  | `isolation`| `host` | Where agents run: the worktree shell, or a docker sandbox. |
43
- | `harness` | `claude` | Which agent `create`/`enter` launch by default. |
43
+ | `harness` | `claude` | Which agent `create` launches by default; `enter` reuses the harness the worktree was created with unless `--harness` overrides it. |
44
44
  | `base` | `main` | Base branch for new branches (resolved as `origin/<base>`). |
45
45
  | `root` | `.treebox/worktrees` | Where worktree directories are created, relative to the repo. |
46
46
  | `env_file` | `.env` | The secrets file copied into every new worktree. |
@@ -66,7 +66,9 @@ cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.a
66
66
  syntax, with `${workspaceName}` substituted per worktree), and `runArgs` feed
67
67
  `docker run`; `postCreate` is the setup command exec'd in the workspace after
68
68
  the container starts. The firewall overlay in `firewall.json` deep-merges on
69
- top when `--firewall` is set.
69
+ top when the firewall is enabled (`firewall = true` in the config, or
70
+ `--firewall` per run; `--no-firewall` opts a single run out of a config
71
+ default).
70
72
 
71
73
  Templates in `~/.config/treebox/templates/<name>` are picked by the
72
74
  `template` config key or per-invocation with `--template <name>`. The
@@ -75,11 +77,51 @@ mount — so a sandboxed agent can't edit it. Any container config in the
75
77
  target repo itself is deliberately ignored; see
76
78
  [how it works](how-it-works.md#the-sandbox-config-lives-outside-the-box).
77
79
 
80
+ ### Worked example: a Node sandbox, not Python
81
+
82
+ Say your project is Node and you want a box with nothing to do with the
83
+ default Python one. Keep a separate named template and select it per run —
84
+ a `node` box and a `python` box coexist, one per stack. Copy the template out:
85
+
86
+ ```bash
87
+ cp -R "$(python -c 'import treebox.assets, pathlib; print(pathlib.Path(treebox.assets.__file__).parent)')/container" \
88
+ ~/.config/treebox/templates/node
89
+ ```
90
+
91
+ The shipped image already bundles Node 22 (plus uv, `gh`, ripgrep, and the
92
+ agent CLIs), so a Node project often needs no image change at all. Add any
93
+ **global** tooling to `~/.config/treebox/templates/node/Dockerfile`:
94
+
95
+ ```dockerfile
96
+ USER root
97
+ RUN npm install -g pnpm@9 typescript tsx
98
+ USER ${USERNAME}
99
+ ```
100
+
101
+ Then the one docker-specific step: point `postCreate` in `container.json` at
102
+ your project's install command. In docker isolation, dependency setup is
103
+ driven by the template's `postCreate` — not the host-mode ecosystem
104
+ auto-detect — so a non-Python project must wire its own install here (the
105
+ npm/pnpm cache is mounted, so it stays warm across worktrees):
106
+
107
+ ```json
108
+ "postCreate": "if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else npm install; fi"
109
+ ```
110
+
111
+ Now provision with it, per-invocation or as your default (`template = "node"`):
112
+
113
+ ```bash
114
+ treebox create my-feature --isolation docker --template node
115
+ ```
116
+
117
+ First run builds your image (cached after that), provisions the worktree, runs
118
+ `postCreate` to install deps, and launches the agent inside.
119
+
78
120
  ## Environment variables
79
121
 
80
122
  | Variable | Effect |
81
123
  | ----------------- | ------------------------------------------------------------- |
82
- | `TREEBOX_CONFIG` | Explicit path to the config file (overrides the XDG lookup). |
124
+ | `TREEBOX_CONFIG` | Explicit path to the config file (overrides the XDG lookup). Setting it asserts the file exists — a missing file here is a loud error (exit `2`), not a silent fall-back to defaults. |
83
125
  | `XDG_CONFIG_HOME` | Standard XDG base for `treebox/config.toml` and templates. |
84
126
  | `XDG_CACHE_HOME` | Standard XDG base for the shared package caches treebox mounts. |
85
127
 
@@ -104,5 +104,7 @@ treebox assumes the caller is often another program:
104
104
  - **`--json` with a `schemaVersion`** that only gains fields within a version
105
105
  (git-porcelain discipline), plus `--print` and `--dry-run` for scripts that
106
106
  want the commands, not the side effects.
107
- - **Per-worktree locking**, so two concurrent `create fix-auth` calls
108
- conflict cleanly (`5`) instead of corrupting a worktree.
107
+ - **Per-worktree locking**, held by `create`, `enter`, and `teardown` alike,
108
+ so racing operations on one name — two `create fix-auth` calls, or a
109
+ `teardown fix-auth` against a concurrent provision — conflict cleanly (`5`)
110
+ instead of corrupting or half-removing a worktree.
@@ -28,13 +28,14 @@ sandbox.
28
28
  <div class="tx-terminal">
29
29
  <div class="tx-terminal__bar"><span class="tx-dot tx-dot--r"></span><span class="tx-dot tx-dot--y"></span><span class="tx-dot tx-dot--g"></span><button class="tx-copy" type="button" data-copy="treebox create --isolation docker" aria-label="Copy command"><svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path fill="currentColor" d="M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12z"/></svg><span class="tx-copy__done">✓</span></button></div>
30
30
  <div class="tx-screen">
31
- <div class="t-line" style="--d:0s"><span class="c-ok">$ </span><span class="t-type">treebox create --isolation docker</span></div>
31
+ <div class="t-scene" data-dur="8600">
32
+ <div class="t-line" style="--d:0s;--n:33"><span class="c-ok">$ </span><span class="t-type">treebox create --isolation docker</span></div>
32
33
  <div class="t-line" style="--d:1.8s"> </div>
33
34
  <div class="t-line" style="--d:1.8s"> <span class="c-acc">●</span> create <b>brave-otter</b></div>
34
35
  <div class="t-line" style="--d:2.0s"> </div>
35
36
  <div class="t-line" style="--d:2.0s"> <span class="c-dim">branch</span> treebox/brave-otter <span class="c-dim">· placeholder — rename before push</span></div>
36
37
  <div class="t-line" style="--d:2.1s"> <span class="c-dim">base</span> main</div>
37
- <div class="t-line" style="--d:2.2s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> claude</div>
38
+ <div class="t-line" style="--d:2.2s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> claude</div>
38
39
  <div class="t-line" style="--d:2.4s"> </div>
39
40
  <div class="t-line" style="--d:2.4s;--run:.8s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> fetch <span class="c-dim">origin up to date</span></div>
40
41
  <div class="t-line" style="--d:3.2s;--run:.5s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> worktree <span class="c-dim">.treebox/worktrees/brave-otter</span></div>
@@ -45,6 +46,58 @@ sandbox.
45
46
  <div class="t-line" style="--d:6.9s"> </div>
46
47
  <div class="t-line" style="--d:6.9s"> <span class="c-ok">Ready</span> <span class="c-dim">in 22.6s — launching</span> claude</div>
47
48
  </div>
49
+ <div class="t-scene" data-dur="5800">
50
+ <div class="t-line" style="--d:0s;--n:34"><span class="c-ok">$ </span><span class="t-type">treebox enter brave-otter -H codex</span></div>
51
+ <div class="t-line" style="--d:1.7s"> </div>
52
+ <div class="t-line" style="--d:1.7s"> <span class="c-acc">●</span> enter <b>brave-otter</b> <span class="c-dim">· feature/user-auth</span></div>
53
+ <div class="t-line" style="--d:1.9s"> </div>
54
+ <div class="t-line" style="--d:1.9s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> codex</div>
55
+ <div class="t-line" style="--d:2.1s"> </div>
56
+ <div class="t-line" style="--d:2.2s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> secrets <span class="c-dim">copied</span></div>
57
+ <div class="t-line" style="--d:2.7s"> <span class="c-dim">· deps unchanged · skipping setup</span></div>
58
+ <div class="t-line" style="--d:2.9s"> </div>
59
+ <div class="t-line" style="--d:3.0s"> <span class="c-ok">Ready</span> <span class="c-dim">in 1.4s — launching</span> codex</div>
60
+ <div class="t-line" style="--d:4.4s"> </div>
61
+ <div class="t-line" style="--d:4.5s">^C</div>
62
+ </div>
63
+ <div class="t-scene" data-dur="4800">
64
+ <div class="t-line" style="--d:0s;--n:35"><span class="c-ok">$ </span><span class="t-type">treebox enter brave-otter -H claude</span></div>
65
+ <div class="t-line" style="--d:1.8s"> </div>
66
+ <div class="t-line" style="--d:1.8s"> <span class="c-acc">●</span> enter <b>brave-otter</b> <span class="c-dim">· feature/user-auth</span></div>
67
+ <div class="t-line" style="--d:2.0s"> </div>
68
+ <div class="t-line" style="--d:2.0s"> <span class="c-dim">isolation</span> docker <span class="c-dim">→</span> claude</div>
69
+ <div class="t-line" style="--d:2.2s"> </div>
70
+ <div class="t-line" style="--d:2.3s;--run:.3s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> secrets <span class="c-dim">copied</span></div>
71
+ <div class="t-line" style="--d:2.8s"> <span class="c-dim">· deps unchanged · skipping setup</span></div>
72
+ <div class="t-line" style="--d:3.0s"> </div>
73
+ <div class="t-line" style="--d:3.1s"> <span class="c-ok">Ready</span> <span class="c-dim">in 1.2s — launching</span> claude</div>
74
+ </div>
75
+ <div class="t-scene" data-dur="4800">
76
+ <div class="t-line" style="--d:0s;--n:10"><span class="c-ok">$ </span><span class="t-type">treebox ls</span></div>
77
+ <div class="t-line" style="--d:1.0s"> </div>
78
+ <div class="t-line" style="--d:1.1s"> <span class="c-dim">NAME BRANCH LAST COMMIT AGE DEPS ENV</span></div>
79
+ <div class="t-line" style="--d:1.1s"> <span class="c-dim">─────────────────────────────────────────────────────────────────────────────</span></div>
80
+ <div class="t-line" style="--d:1.3s"> brave-otter feature/user-auth <span class="c-dim">feat: add signup flow 5m</span> <span class="c-ok">● fresh</span> <span class="c-ok">● present</span></div>
81
+ </div>
82
+ <div class="t-scene" data-dur="6600">
83
+ <div class="t-line" style="--d:0s;--n:22"><span class="c-ok">$ </span><span class="t-type">treebox rm brave-otter</span></div>
84
+ <div class="t-line" style="--d:1.4s;--td:2s">Remove worktree brave-otter? [y/N]: <span class="t-in">y</span></div>
85
+ <div class="t-line" style="--d:2.5s"> </div>
86
+ <div class="t-line" style="--d:2.5s"> <span class="c-acc">●</span> teardown <b>brave-otter</b></div>
87
+ <div class="t-line" style="--d:2.7s"> </div>
88
+ <div class="t-line" style="--d:2.8s;--run:.7s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> container <span class="c-dim">removed treebox-brave-otter</span></div>
89
+ <div class="t-line" style="--d:3.7s;--run:.25s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> sandbox <span class="c-dim">removed</span></div>
90
+ <div class="t-line" style="--d:4.1s;--run:.35s"> <span class="t-g"><span class="t-spin"></span><span class="t-check">✓</span></span> worktree <span class="c-dim">removed .treebox/worktrees/brave-otter</span></div>
91
+ <div class="t-line" style="--d:4.7s"> <span class="c-dim">· branch kept feature/user-auth</span></div>
92
+ </div>
93
+ <div class="t-scene" data-dur="5200">
94
+ <div class="t-line" style="--d:0s;--n:10"><span class="c-ok">$ </span><span class="t-type">treebox ls</span></div>
95
+ <div class="t-line" style="--d:0.9s"> </div>
96
+ <div class="t-line" style="--d:1.0s"> <span class="c-dim">no worktrees yet — create one with</span> <span class="c-acc">treebox create</span></div>
97
+ <div class="t-line" style="--d:1.8s"> </div>
98
+ <div class="t-line" style="--d:2.1s"><span class="c-ok">$ </span><span class="t-cur"></span></div>
99
+ </div>
100
+ </div>
48
101
  </div>
49
102
 
50
103
  No branch name needed up front: each worktree gets its own directory and a
@@ -140,7 +193,7 @@ Docker isolation needs exactly one extra thing: Docker — see
140
193
  ```bash
141
194
  treebox doctor # verify the host is ready
142
195
  treebox create # provision + launch claude (name generated)
143
- treebox enter brave-otter --harness codex # re-enter later, pick agent per entry
196
+ treebox enter brave-otter --harness codex # re-enter later; -H overrides the recorded harness for this session
144
197
  treebox list # what exists, what's stale
145
198
  treebox teardown brave-otter --delete-branch
146
199
  ```
@@ -118,6 +118,7 @@ The `install.sh` script reads a few environment variables:
118
118
  ```bash
119
119
  git clone https://github.com/Seth-Peters/treebox && cd treebox
120
120
  uv run treebox ... # run the CLI from the working tree
121
+ uv run --extra dev pre-commit install # lint/format/type hooks (see CONTRIBUTING.md)
121
122
  uv run --extra dev python -m pytest # unit + integration suite
122
- ./scripts/validate.sh # lint + tests + live host-runner smoke
123
+ ./scripts/validate.sh # lint + format + tests + live host-runner smoke
123
124
  ```
@@ -4,30 +4,36 @@
4
4
  * 2. Console code blocks: the Material copy button copies only the
5
5
  * `$ `-prefixed commands, never the output printed under them.
6
6
  * 3. ✓ / ✗ marks in console output get their CLI colors.
7
- * 4. The hero terminal replays its `treebox create --isolation docker` run in a loop,
8
- * starting when it scrolls into view. Without JS or with reduced
9
- * motion, the static final frame shows instead.
7
+ * 4. The hero terminal plays the full treebox lifecycle in a loop — create,
8
+ * enter (codex), ^C, enter (claude), ls, rm, done one .t-scene per
9
+ * command, each shown for its data-dur, starting when the terminal
10
+ * scrolls into view. Without JS or with reduced motion, the first
11
+ * scene shows as a static frame instead.
10
12
  * 5. A "Copy page" button at the top of every page copies that page's raw
11
13
  * Markdown source (embedded by hooks/copy_page.py) — an agent-ready copy,
12
14
  * entirely offline.
13
15
  */
14
16
  (function () {
15
- var PLAY_MS = 7000; // last line lands at ~6.9s
16
- var HOLD_MS = 3800; // linger on the final frame before replaying
17
-
18
17
  function animateTerminals() {
19
18
  document.querySelectorAll(".tx-terminal").forEach(function (term) {
20
19
  if (!term.querySelector(".tx-screen") || term.dataset.anim) return;
21
20
  term.dataset.anim = "1";
22
21
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
22
+ var scenes = term.querySelectorAll(".t-scene");
23
+ if (!scenes.length) return;
23
24
  term.classList.add("tx-anim");
24
25
 
25
- function play() {
26
- if (!term.isConnected) return;
27
- term.classList.remove("is-playing");
28
- void term.offsetWidth; // reflush so the animations restart from 0
29
- term.classList.add("is-playing");
30
- setTimeout(play, PLAY_MS + HOLD_MS);
26
+ function show(i) {
27
+ if (!term.isConnected) return; // page swapped out — stop the loop
28
+ scenes.forEach(function (s) {
29
+ s.classList.remove("is-on");
30
+ });
31
+ void term.offsetWidth; // reflush so the scene's animations restart from 0
32
+ scenes[i].classList.add("is-on");
33
+ var dur = parseInt(scenes[i].getAttribute("data-dur"), 10) || 5000;
34
+ setTimeout(function () {
35
+ show((i + 1) % scenes.length);
36
+ }, dur);
31
37
  }
32
38
 
33
39
  var io = new IntersectionObserver(
@@ -35,7 +41,8 @@
35
41
  entries.forEach(function (e) {
36
42
  if (e.isIntersecting) {
37
43
  io.disconnect();
38
- play();
44
+ term.classList.add("is-playing");
45
+ show(0);
39
46
  }
40
47
  });
41
48
  },