warmtree 0.1.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.
- warmtree-0.1.0/.gitattributes +1 -0
- warmtree-0.1.0/.github/workflows/ci.yml +23 -0
- warmtree-0.1.0/.github/workflows/publish.yml +39 -0
- warmtree-0.1.0/.gitignore +9 -0
- warmtree-0.1.0/.python-version +1 -0
- warmtree-0.1.0/CLAUDE.md +25 -0
- warmtree-0.1.0/DESIGN.md +143 -0
- warmtree-0.1.0/IDEAS.md +11 -0
- warmtree-0.1.0/LICENSE +21 -0
- warmtree-0.1.0/PKG-INFO +279 -0
- warmtree-0.1.0/README.md +259 -0
- warmtree-0.1.0/docs/demo.gif +0 -0
- warmtree-0.1.0/pyproject.toml +46 -0
- warmtree-0.1.0/tests/conftest.py +28 -0
- warmtree-0.1.0/tests/test_cli.py +106 -0
- warmtree-0.1.0/tests/test_cli_lifecycle.py +174 -0
- warmtree-0.1.0/tests/test_cli_refresh.py +76 -0
- warmtree-0.1.0/tests/test_config.py +103 -0
- warmtree-0.1.0/tests/test_git.py +62 -0
- warmtree-0.1.0/tests/test_lifecycle.py +253 -0
- warmtree-0.1.0/tests/test_lock.py +43 -0
- warmtree-0.1.0/tests/test_pool.py +79 -0
- warmtree-0.1.0/tests/test_refresh.py +184 -0
- warmtree-0.1.0/tests/test_size.py +179 -0
- warmtree-0.1.0/tests/test_skill.py +172 -0
- warmtree-0.1.0/tests/test_state_concurrency.py +48 -0
- warmtree-0.1.0/tests/test_warm.py +102 -0
- warmtree-0.1.0/uv.lock +108 -0
- warmtree-0.1.0/warmtree/__init__.py +3 -0
- warmtree-0.1.0/warmtree/__main__.py +5 -0
- warmtree-0.1.0/warmtree/cli.py +344 -0
- warmtree-0.1.0/warmtree/config.py +142 -0
- warmtree-0.1.0/warmtree/git.py +152 -0
- warmtree-0.1.0/warmtree/lock.py +59 -0
- warmtree-0.1.0/warmtree/pool.py +365 -0
- warmtree-0.1.0/warmtree/skill.py +93 -0
- warmtree-0.1.0/warmtree/skills/warmtree/SKILL.md +116 -0
- warmtree-0.1.0/warmtree/warm.py +104 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest, windows-latest]
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: uv sync
|
|
21
|
+
- run: uv run ruff check .
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag is pushed, using trusted publishing.
|
|
4
|
+
# No API token is stored anywhere; PyPI trusts this workflow's OIDC identity.
|
|
5
|
+
# One-time setup on pypi.org: add a trusted publisher for
|
|
6
|
+
# owner dcolliervb23, repository warmtree, workflow publish.yml, environment pypi.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- name: Tag must match the package version
|
|
21
|
+
run: test "v$(uv version --short)" = "$GITHUB_REF_NAME"
|
|
22
|
+
- run: uv build
|
|
23
|
+
- uses: actions/upload-artifact@v4
|
|
24
|
+
with:
|
|
25
|
+
name: dist
|
|
26
|
+
path: dist/
|
|
27
|
+
|
|
28
|
+
publish:
|
|
29
|
+
needs: build
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment: pypi
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/download-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
warmtree-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# warmtree — rules for AI sessions in this repo
|
|
2
|
+
|
|
3
|
+
Read DESIGN.md first. It is the scope. Do not expand it; append ideas to IDEAS.md.
|
|
4
|
+
The scope is the pool only. Do not add port allocation, merge helpers, or
|
|
5
|
+
general cleanup; the design explains why.
|
|
6
|
+
|
|
7
|
+
## Working rules
|
|
8
|
+
- Prefer boring, readable Python over clever Python. Every line should be one
|
|
9
|
+
a reviewer can follow without explanation.
|
|
10
|
+
- The maintainer writes the tests and the README. Draft them if asked, then
|
|
11
|
+
stop and wait for review before committing.
|
|
12
|
+
- Small commits, one step each. Conventional messages (`feat:`, `test:`, `docs:`).
|
|
13
|
+
Plain messages: no co-author trailers, no session links.
|
|
14
|
+
|
|
15
|
+
## Publishing hygiene
|
|
16
|
+
- `git config user.email dcolliervb23@users.noreply.github.com` in this repo. Never a personal email.
|
|
17
|
+
- No secrets. PyPI publishing is GitHub Actions trusted publishing only.
|
|
18
|
+
- No local machine paths in any committed file.
|
|
19
|
+
- Assume every commit is public.
|
|
20
|
+
|
|
21
|
+
## Stack
|
|
22
|
+
- Python 3.12, standard library only at runtime (`dependencies = []`).
|
|
23
|
+
- Dev tools: `uv`, `ruff`, `pytest`. Tests create real temporary git repos.
|
|
24
|
+
- Run tests with `uv run pytest -q`. Lint with `uv run ruff check .`.
|
|
25
|
+
- Must work on Windows, Linux, and WSL. CI runs on ubuntu-latest and windows-latest.
|
warmtree-0.1.0/DESIGN.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# warmtree design
|
|
2
|
+
|
|
3
|
+
> `git worktree add` is fast. Everything after it is slow: dependency install,
|
|
4
|
+
> build caches, untracked env files. warmtree keeps a few worktrees already
|
|
5
|
+
> checked out, fast-forwarded, and warmed, so `warmtree take <branch>` hands
|
|
6
|
+
> you a ready workspace in under a second. Built for parallel AI coding agents.
|
|
7
|
+
|
|
8
|
+
Language: Python 3.12, standard library only.
|
|
9
|
+
|
|
10
|
+
## Positioning
|
|
11
|
+
|
|
12
|
+
The worktree-tooling space is crowded. worktrunk (Rust) already does on-create
|
|
13
|
+
hooks, copies ignored files, allocates ports, and cleans up merged worktrees.
|
|
14
|
+
Several smaller tools cover env copying and ports on their own. **No tool
|
|
15
|
+
pre-creates warm worktrees.** The pool is the entire contribution, and the
|
|
16
|
+
scope is held to it on purpose.
|
|
17
|
+
|
|
18
|
+
warmtree owns the **create** and **release** steps. Everything in between is
|
|
19
|
+
ordinary git, so worktrunk, portree, global git hooks, and editors all work on
|
|
20
|
+
a pooled worktree unchanged. The README documents two workflows: standalone,
|
|
21
|
+
and warmtree-for-create plus worktrunk-for-everything-else.
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- Standard library only. No runtime dependencies. `uvx warmtree` must work
|
|
26
|
+
on a machine with nothing but Python and git.
|
|
27
|
+
- Stack-agnostic. warmtree never knows what npm or dotnet are. Projects
|
|
28
|
+
declare their own warm commands.
|
|
29
|
+
- Boring, readable Python. Every line should be explainable to a reviewer.
|
|
30
|
+
- Tests run against real temporary git repos. Nothing about git is mocked.
|
|
31
|
+
|
|
32
|
+
## v1 scope
|
|
33
|
+
|
|
34
|
+
### Config: `.warmtree.toml` at the repo root (parsed with `tomllib`)
|
|
35
|
+
|
|
36
|
+
```toml
|
|
37
|
+
[pool]
|
|
38
|
+
size = 3 # slots to keep ready
|
|
39
|
+
base = "main" # branch slots are parked on
|
|
40
|
+
dir = "../.warmtree/app" # where slots live; default is ../.warmtree/<repo name>
|
|
41
|
+
lockfiles = ["package-lock.json", "uv.lock"] # re-warm only when these change
|
|
42
|
+
|
|
43
|
+
[warm]
|
|
44
|
+
run = ["npm ci"] # executed in the slot at fill/refresh time
|
|
45
|
+
copy = [".env", ".env.local"] # untracked files copied from the main worktree
|
|
46
|
+
env = true # write WARMTREE_SLOT=<n> into copied env files
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Every key has a default; an empty file means "pool of 2 on the default
|
|
50
|
+
branch, fast-forward only."
|
|
51
|
+
|
|
52
|
+
### Commands
|
|
53
|
+
| Command | Behavior |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `warmtree init` | Write a starter config. Detects common lockfiles only to pre-fill `lockfiles`; never guesses `run`. Installs the agent skill for any coding agent the repo shows signs of. |
|
|
56
|
+
| `warmtree skill [--tool T] [--force]` | Install or refresh the agent skill in detected or named tool folders. |
|
|
57
|
+
| `warmtree fill` | Create missing slots with `git worktree add --detach`, run `copy`, run `run`, record lockfile hashes. |
|
|
58
|
+
| `warmtree take <branch> [--from <ref>]` | Claim the oldest ready slot: create or check out `<branch>` inside it, print its path, mark it taken. Refill the pool afterward (`--no-refill` to skip; `--refill-background` spawns a detached process). If no slot is ready, fall back to a cold create and say so. |
|
|
59
|
+
| `warmtree release <branch>` | Reset the slot to `base`, discard the working tree (`--keep-branch` keeps the branch ref), return the slot to ready. Refuses if the tree is dirty unless `--force`. |
|
|
60
|
+
| `warmtree refresh` | Fast-forward every ready slot to `base`; if any lockfile hash changed, re-run `run` and `copy` in that slot. Meant for a nightly scheduled task or cron. |
|
|
61
|
+
| `warmtree status` | Table of slots: path, state (ready/taken/warming/stale), branch, age, last warm. `--json` for agents. |
|
|
62
|
+
| `warmtree size [N]` | Show configured size and counts by state. With `N`, write the size to the config and grow or shrink the pool to match; shrinking removes waiting slots only. |
|
|
63
|
+
| `warmtree which` | Name the slot the current directory is inside, so an agent can tell whether it already has a workspace. |
|
|
64
|
+
| `warmtree remove [SLOT...] [--all]` | Delete slots and their worktree registrations. |
|
|
65
|
+
|
|
66
|
+
### State
|
|
67
|
+
- `state.json` inside the pool dir: slot list with state, branch,
|
|
68
|
+
created/warmed timestamps, lockfile hashes. One file, rewritten atomically.
|
|
69
|
+
- Concurrency: `take` holds an OS file lock (`msvcrt` on Windows, `fcntl`
|
|
70
|
+
elsewhere) for the claim. Two agents calling `take` at once get two
|
|
71
|
+
different slots.
|
|
72
|
+
|
|
73
|
+
### Agent integration
|
|
74
|
+
- `warmtree/skills/warmtree/SKILL.md` in the open Agent Skills format (read by
|
|
75
|
+
Claude Code, GitHub Copilot, Codex CLI, Cursor), shipped inside the package.
|
|
76
|
+
`warmtree init` copies it into the skills folder of every tool the repo
|
|
77
|
+
shows signs of using (`CLAUDE.md`, `copilot-instructions.md`, `AGENTS.md`,
|
|
78
|
+
`.cursor/`); `warmtree skill` does the same on demand and never overwrites
|
|
79
|
+
an edited copy without `--force`. The rule: when an isolated workspace is needed,
|
|
80
|
+
run `warmtree take <branch>` and `cd` into the printed path instead of
|
|
81
|
+
`git worktree add`; when done and merged, `warmtree release <branch>`.
|
|
82
|
+
Before that, `warmtree which` tells the agent whether it is already in a
|
|
83
|
+
slot, and `warmtree size` tells it whether enough slots are ready for the
|
|
84
|
+
parallel work it is about to start, and grows the pool if not.
|
|
85
|
+
- `AGENTS.md` paragraph in the README for projects that use that convention.
|
|
86
|
+
- Note in the README that Claude Code's native worktree feature is not
|
|
87
|
+
intercepted; the skill is the integration.
|
|
88
|
+
|
|
89
|
+
### Out (v1)
|
|
90
|
+
- Port allocation beyond the `WARMTREE_SLOT` variable. Subdomain routing.
|
|
91
|
+
Merging, PR creation, branch listing. Any GUI or TUI. Daemon mode. Shared
|
|
92
|
+
dependency stores. Windows junction tricks for `node_modules`.
|
|
93
|
+
|
|
94
|
+
## Architecture
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
warmtree/
|
|
98
|
+
__init__.py
|
|
99
|
+
cli.py argparse entry point, one function per command
|
|
100
|
+
config.py load/validate .warmtree.toml, defaults
|
|
101
|
+
git.py thin subprocess wrapper: worktree add/remove, fetch, ff, status
|
|
102
|
+
pool.py slot lifecycle: fill, take, release, refresh, state persistence
|
|
103
|
+
lock.py cross-platform file lock
|
|
104
|
+
warm.py run commands, copy files, hash lockfiles, inject WARMTREE_SLOT
|
|
105
|
+
skill.py detect coding-agent folders, install the bundled SKILL.md
|
|
106
|
+
skills/warmtree/SKILL.md
|
|
107
|
+
tests/ pytest against real temporary git repos (no mocks of git)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Key decisions and the reasoning behind each:
|
|
111
|
+
- **Slots are detached worktrees parked on `base`.** A branch is only
|
|
112
|
+
created at `take`, so a slot is never "on" a branch anyone else is using,
|
|
113
|
+
and git's one-branch-per-worktree rule never bites the pool.
|
|
114
|
+
- **Reset on release, do not delete.** Deleting and recreating throws away
|
|
115
|
+
the warm state; resetting keeps `node_modules` and friends intact. Lockfile
|
|
116
|
+
hashes decide whether a re-warm is needed.
|
|
117
|
+
- **Fallback to cold create.** An empty pool is a slow path, never an error.
|
|
118
|
+
Agents should never fail because the pool drained.
|
|
119
|
+
- **State in one JSON file with a lock, not SQLite.** Dozens of slots at
|
|
120
|
+
most; simplest thing that is correct under concurrent `take`.
|
|
121
|
+
- **Standard library only.** The tool has to run before the project's own
|
|
122
|
+
dependencies exist; that is the whole point.
|
|
123
|
+
|
|
124
|
+
## Build order
|
|
125
|
+
|
|
126
|
+
| # | Deliverable | Done when |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| 1 | config, git wrapper, `init`, `fill`, `status`, tests on temp repos, CI on ubuntu + windows | `fill` creates N detached slots in a temp repo; CI green on both OSes |
|
|
129
|
+
| 2 | `take` with file lock and refill, `release` with reset, `remove`, fallback path | Two concurrent `take` calls get two slots (test); `release` returns a slot to ready |
|
|
130
|
+
| 3 | `warm`: run commands, copy files, lockfile hashing, `refresh`, `WARMTREE_SLOT` | Changing a lockfile makes `refresh` re-run `run` in every slot, unchanged lockfile skips |
|
|
131
|
+
| 4 | SKILL.md, AGENTS.md, README with GIF and worktrunk workflow, trusted publishing to PyPI, tag v0.1.0 | `uvx warmtree status` works on a clean machine; Claude Code takes a slot via the skill |
|
|
132
|
+
|
|
133
|
+
## Acceptance criteria (v0.1.0)
|
|
134
|
+
- 25+ tests against real git repos, green in CI on Ubuntu and Windows.
|
|
135
|
+
- `take` returns a ready slot in under 1 second on a repo whose `npm ci` takes minutes (measured in the README).
|
|
136
|
+
- Concurrency test in step 2 passes.
|
|
137
|
+
- Zero runtime dependencies; `pyproject.toml` `dependencies = []`.
|
|
138
|
+
- PyPI release published from GitHub Actions via trusted publishing; no API token stored locally.
|
|
139
|
+
|
|
140
|
+
## Before publishing
|
|
141
|
+
Use it on a large real repo with `size = 2` for a week before the first
|
|
142
|
+
release. Every papercut found goes in IDEAS.md; only bugs are fixed before
|
|
143
|
+
v0.1.0.
|
warmtree-0.1.0/IDEAS.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Ideas
|
|
2
|
+
|
|
3
|
+
Things that came up while building warmtree and are out of scope for v1.
|
|
4
|
+
Append here instead of expanding DESIGN.md.
|
|
5
|
+
|
|
6
|
+
- **Stream warm output.** `run` output is captured and shown only on failure
|
|
7
|
+
so `take` keeps stdout clean. A `--verbose` flag on `fill` and `refresh`
|
|
8
|
+
could stream it to stderr for a first-time `npm ci` that takes minutes.
|
|
9
|
+
- **`refresh --fetch`.** `refresh` moves slots to the local base tip. It could
|
|
10
|
+
run `git fetch` first, or accept `base = "origin/main"`, which already works
|
|
11
|
+
because the base is resolved with `rev-parse`.
|
warmtree-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan Collier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
warmtree-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: warmtree
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pool of pre-warmed git worktrees for parallel AI coding agents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dcolliervb23/warmtree
|
|
6
|
+
Author: Dan Collier
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,developer-tools,git,worktree
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# warmtree
|
|
22
|
+
|
|
23
|
+
A pool of pre-warmed git worktrees for parallel AI coding agents.
|
|
24
|
+
|
|
25
|
+
`git worktree add` is fast. Everything after it is slow: installing
|
|
26
|
+
dependencies, rebuilding caches, copying the `.env` files git does not track.
|
|
27
|
+
On a large repo that cold start is minutes, and with several agents working in
|
|
28
|
+
parallel you pay it several times a day. warmtree keeps a few worktrees already
|
|
29
|
+
checked out, warmed, and ready, so `warmtree take <branch>` hands you a
|
|
30
|
+
workspace in under a second.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
Measured on a private TypeScript app (457 packages, 664 MB of node_modules):
|
|
35
|
+
a cold `git worktree add` + `npm ci` took 6.6 s even with a fast machine and
|
|
36
|
+
network; `warmtree take` handed over a warm slot in 0.15 s.
|
|
37
|
+
|
|
38
|
+
warmtree owns the **create** and **release** steps only. Everything in between
|
|
39
|
+
is ordinary git, so your editor, hooks, and other worktree tools work on a
|
|
40
|
+
pooled worktree unchanged.
|
|
41
|
+
|
|
42
|
+
## Status
|
|
43
|
+
|
|
44
|
+
Early. Stable enough to dogfood, and dogfooded daily on a large private repo.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
Requires Python 3.12+ and git. No other runtime dependencies.
|
|
49
|
+
|
|
50
|
+
From a clone, for dogfooding while the code is still changing:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uv tool install --editable .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or straight from GitHub:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
uv tool install git+https://github.com/dcolliervb23/warmtree
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or from PyPI: `uvx warmtree` works on a machine with nothing but Python
|
|
63
|
+
and git.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
Run these from inside the repo you want to pool.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
warmtree init # writes .warmtree.toml with defaults
|
|
71
|
+
# edit .warmtree.toml: add your install command to [warm] run
|
|
72
|
+
warmtree fill # creates and warms the slots, two by default
|
|
73
|
+
warmtree status # see them
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Take a slot when you need an isolated workspace, work in it, then release it:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
cd "$(warmtree take feature/login)" # bash / zsh
|
|
80
|
+
# ... commit, push, open a PR ...
|
|
81
|
+
warmtree release feature/login
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
PowerShell:
|
|
85
|
+
|
|
86
|
+
```powershell
|
|
87
|
+
cd (warmtree take feature/login)
|
|
88
|
+
warmtree release feature/login
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`take` prints only the slot path on stdout. Everything else it says goes to
|
|
92
|
+
stderr, so the `cd` idiom works.
|
|
93
|
+
|
|
94
|
+
Keep slots current with a nightly `warmtree refresh` from cron or Task
|
|
95
|
+
Scheduler, after whatever pulls your base branch.
|
|
96
|
+
|
|
97
|
+
## Commands
|
|
98
|
+
|
|
99
|
+
| Command | What it does |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `warmtree init [--force] [--no-skill]` | Write a starter `.warmtree.toml`. Pre-fills `lockfiles` from what it finds in the repo. Never guesses your install command. Installs the agent skill for any coding agent it detects. |
|
|
102
|
+
| `warmtree skill [--tool T] [--force]` | Install or refresh the agent skill in the tool folders this repo uses, or in the ones named with `--tool`. Never overwrites an edited copy without `--force`. |
|
|
103
|
+
| `warmtree fill` | Create slots until `size` are ready. Each slot is a worktree with a detached HEAD at the base branch, with `copy` files copied in and `run` commands executed. |
|
|
104
|
+
| `warmtree take <branch> [--from REF]` | Claim the oldest ready slot. Creates `<branch>` there (from `REF` or the base branch) or checks it out if it already exists. Prints the path, then refills the pool. |
|
|
105
|
+
| `warmtree take ... --no-refill` | Skip the refill. |
|
|
106
|
+
| `warmtree take ... --refill-background` | Refill in a detached process and return immediately. |
|
|
107
|
+
| `warmtree release <branch> [--keep-branch] [--force]` | Park the slot back on the base branch and mark it ready. Refuses a dirty tree unless `--force`. Deletes the branch if it is merged; an unmerged branch is always kept. |
|
|
108
|
+
| `warmtree refresh` | Move every waiting slot to the current base commit and re-copy files. Re-runs `run` only in slots whose lockfile hashes changed or whose last warm failed. Skips taken slots. |
|
|
109
|
+
| `warmtree size [N]` | Show the configured size and a count of slots by state. With `N`, write the new size to `.warmtree.toml` and grow or shrink the pool to match. Shrinking removes ready slots only. |
|
|
110
|
+
| `warmtree which` | Name the slot the current directory is inside, as `slot-N <state> <branch>`. Exit 1 if not in a slot. |
|
|
111
|
+
| `warmtree remove [SLOT...] [--all] [--force]` | Delete slots and their worktree registrations. Taken slots need `--force`. |
|
|
112
|
+
| `warmtree status [--json]` | Table of slots: name, state, branch, age, last warm, path. `--json` for scripts and agents. |
|
|
113
|
+
|
|
114
|
+
Slot states:
|
|
115
|
+
|
|
116
|
+
- `ready`: parked on base, warm, free to take.
|
|
117
|
+
- `taken`: a branch is checked out and someone is working in it.
|
|
118
|
+
- `warming`: `run` commands are executing right now, in `fill` or `refresh`.
|
|
119
|
+
- `stale`: the last warm failed. `refresh` retries it. `take` never hands out a
|
|
120
|
+
stale slot.
|
|
121
|
+
|
|
122
|
+
If no slot is ready, `take` falls back to a normal `git worktree add`, tells
|
|
123
|
+
you on stderr, and the new worktree joins the pool as a taken slot. An empty
|
|
124
|
+
pool is a slow path, never an error.
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
`.warmtree.toml` at the repo root. Every key has a default; an empty file or
|
|
129
|
+
no file at all means a pool of two on the repo's default branch with nothing
|
|
130
|
+
to warm.
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
[pool]
|
|
134
|
+
size = 2 # slots to keep ready; taken slots do not count
|
|
135
|
+
# base = "main" # branch slots park on; default: the repo's default branch
|
|
136
|
+
# dir = "../.warmtree/app" # where slots live; default: ../.warmtree/<repo name>
|
|
137
|
+
lockfiles = ["package-lock.json", "uv.lock"] # re-warm only when one of these changes
|
|
138
|
+
|
|
139
|
+
[warm]
|
|
140
|
+
run = ["npm ci"] # commands run inside a slot at fill and refresh time
|
|
141
|
+
copy = [".env", ".env.local"] # untracked files copied from the main worktree
|
|
142
|
+
env = true # write WARMTREE_SLOT=<n> into the copied env files
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Details worth knowing:
|
|
146
|
+
|
|
147
|
+
- `run` commands go through the shell, in order, inside the slot, with
|
|
148
|
+
`WARMTREE_SLOT=<n>` in the environment. Output is captured and shown only
|
|
149
|
+
when a command fails. warmtree has no idea what npm or dotnet are; you do.
|
|
150
|
+
- `copy` paths are relative to the repo root. Files missing from the main
|
|
151
|
+
worktree are skipped. With `env = true`, copied files named like `.env`,
|
|
152
|
+
`.env.local`, or `app.env` get a `WARMTREE_SLOT=<n>` line appended, so your
|
|
153
|
+
project can derive a per-slot port or database name from it.
|
|
154
|
+
- `lockfiles` are hashed inside the slot after each warm. `refresh` re-runs
|
|
155
|
+
`run` only when a hash differs.
|
|
156
|
+
- `warmtree size N` edits the `size` line in place. Your comments and other
|
|
157
|
+
keys are left alone.
|
|
158
|
+
- Unknown keys are errors, so a typo never silently disables warming.
|
|
159
|
+
|
|
160
|
+
## Using it with coding agents
|
|
161
|
+
|
|
162
|
+
warmtree ships an [Agent Skill](https://agentskills.io): a short set of
|
|
163
|
+
instructions an agent loads when the task calls for an isolated workspace. It
|
|
164
|
+
tells the agent to check whether it is already in a slot (`warmtree which`),
|
|
165
|
+
check capacity before fanning out (`warmtree size`), take a slot instead of
|
|
166
|
+
running `git worktree add`, and release it when the branch is merged.
|
|
167
|
+
|
|
168
|
+
`warmtree init` installs it automatically wherever it sees signs of a coding
|
|
169
|
+
agent in the repo, and prints each path it wrote:
|
|
170
|
+
|
|
171
|
+
| Found in the repo | Skill installed at |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `CLAUDE.md` or `.claude/` | `.claude/skills/warmtree/SKILL.md` (Claude Code) |
|
|
174
|
+
| `.github/copilot-instructions.md` | `.github/skills/warmtree/SKILL.md` (GitHub Copilot) |
|
|
175
|
+
| `AGENTS.md` or `.agents/` | `.agents/skills/warmtree/SKILL.md` (Codex and others) |
|
|
176
|
+
| `.cursor/` | `.cursor/skills/warmtree/SKILL.md` (Cursor) |
|
|
177
|
+
|
|
178
|
+
Commit those folders so every agent on the project gets the skill. To install
|
|
179
|
+
for a tool that was not detected, or to refresh the copies after upgrading
|
|
180
|
+
warmtree:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
warmtree skill --tool claude # claude, copilot, codex, or cursor; repeatable
|
|
184
|
+
warmtree skill # re-detect and refresh
|
|
185
|
+
warmtree skill --force # replace a copy you edited by hand
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A copy you have edited is never overwritten without `--force`. Pass
|
|
189
|
+
`--no-skill` to `init` to skip all of this.
|
|
190
|
+
|
|
191
|
+
If your project uses an `AGENTS.md` instead, this paragraph is enough:
|
|
192
|
+
|
|
193
|
+
> This repo has a warmtree pool. When you need an isolated workspace, run
|
|
194
|
+
> `warmtree take <branch>` and `cd` into the printed path instead of
|
|
195
|
+
> `git worktree add`. Run `warmtree which` first to see if you are already in
|
|
196
|
+
> a slot, and `warmtree size` to check how many are ready before starting
|
|
197
|
+
> parallel work. When the branch is merged, run `warmtree release <branch>`.
|
|
198
|
+
|
|
199
|
+
Claude Code's built-in worktree feature is not intercepted. The skill is the
|
|
200
|
+
integration.
|
|
201
|
+
|
|
202
|
+
## With worktrunk
|
|
203
|
+
|
|
204
|
+
[worktrunk](https://github.com/max-sixty/worktrunk) covers the rest of the
|
|
205
|
+
worktree lifecycle: hooks, port allocation, cleanup of merged branches. The two
|
|
206
|
+
fit together because warmtree only touches create and release.
|
|
207
|
+
|
|
208
|
+
- Use `warmtree take` instead of `wt switch --create` to get a warm checkout.
|
|
209
|
+
- Inside the slot, worktrunk's commands work as they do in any worktree.
|
|
210
|
+
- When the branch is merged, run `warmtree release <branch>` rather than
|
|
211
|
+
worktrunk's remove, so the slot goes back to the pool instead of being
|
|
212
|
+
deleted.
|
|
213
|
+
|
|
214
|
+
## How it works
|
|
215
|
+
|
|
216
|
+
- **Slots are detached worktrees parked on the base branch.** A branch is only
|
|
217
|
+
created at `take`, so a slot is never on a branch someone else is using and
|
|
218
|
+
git's one-branch-per-worktree rule never bites the pool.
|
|
219
|
+
- **Release resets, it does not delete.** `release` checks out the base
|
|
220
|
+
branch detached and runs `git clean -fd`, which removes untracked files but
|
|
221
|
+
keeps ignored ones. `node_modules`, `.venv`, and friends survive, so the slot
|
|
222
|
+
is still warm for the next take.
|
|
223
|
+
- **Warming never holds the lock.** A slot is marked `warming` under the pool
|
|
224
|
+
lock, the slow commands run with the lock released, and the result is
|
|
225
|
+
written under the lock again. A three-minute `npm ci` in one slot never
|
|
226
|
+
blocks `take` on another.
|
|
227
|
+
- **State is one JSON file with a file lock.** `state.json` lives in the pool
|
|
228
|
+
directory and is rewritten atomically. `take` holds an OS file lock while it
|
|
229
|
+
picks a slot, so two agents calling `take` at the same moment get two
|
|
230
|
+
different slots.
|
|
231
|
+
- **Standard library only.** warmtree has to run before your project's own
|
|
232
|
+
dependencies exist. That is the whole point.
|
|
233
|
+
|
|
234
|
+
The pool directory defaults to a sibling of your repo named after it, for
|
|
235
|
+
example `~/dev/.warmtree/myapp/slot-1`, so repos that share a parent folder
|
|
236
|
+
never share a pool.
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
uv sync # creates .venv with Python 3.12, pytest, ruff
|
|
242
|
+
uv run pytest -q # the whole suite, about a minute
|
|
243
|
+
uv run ruff check .
|
|
244
|
+
uv run ruff format --check .
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Those three commands are exactly what CI runs on Ubuntu and Windows. Tests
|
|
248
|
+
create real temporary git repos; nothing about git is mocked, so `git` must
|
|
249
|
+
be on your PATH.
|
|
250
|
+
|
|
251
|
+
Useful variations:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
uv run pytest -q tests/test_lifecycle.py # one file
|
|
255
|
+
uv run pytest -q -k concurrent # tests whose name matches
|
|
256
|
+
uv run pytest -x # stop at the first failure
|
|
257
|
+
uv run pytest -v # show every test name
|
|
258
|
+
uv run ruff format . # fix formatting instead of checking
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Releasing
|
|
262
|
+
|
|
263
|
+
Releases go to PyPI through GitHub Actions trusted publishing; no API token is
|
|
264
|
+
stored anywhere. One-time setup on pypi.org: add a trusted publisher for owner
|
|
265
|
+
`dcolliervb23`, repository `warmtree`, workflow `publish.yml`, environment
|
|
266
|
+
`pypi`. Then:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
uv version 0.1.0 # sets the version in pyproject.toml
|
|
270
|
+
git commit -am "chore: release 0.1.0"
|
|
271
|
+
git tag v0.1.0
|
|
272
|
+
git push && git push --tags
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The workflow refuses to publish if the tag and the package version disagree.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT
|