docs(website): rewrite worktrees page around scripts, services, and proxy

This commit is contained in:
Mohamed Boudra
2026-04-18 21:15:06 +07:00
parent 93cda8e60f
commit f8815bf1c2
+172 -221
View File
@@ -5,7 +5,7 @@ export const Route = createFileRoute("/docs/worktrees")({
head: () => ({
meta: pageMeta(
"Git Worktrees - Paseo Docs",
"Run agents in isolated git worktrees for parallel feature development.",
"Run agents in isolated git worktrees with setup hooks, scripts, and long-running services.",
),
}),
component: Worktrees,
@@ -25,285 +25,236 @@ function Worktrees() {
<div>
<h1 className="text-3xl font-medium font-title mb-4">Git Worktrees</h1>
<p className="text-white/60 leading-relaxed">
Git worktrees let you have multiple working directories from the same repository. Paseo
uses them to run agents in isolated branches without switching contexts.
Each agent runs in its own git worktree — a separate directory on a separate branch — so
parallel agents never step on each other. You configure setup, scripts, and long-running
services through a <code className="font-mono">paseo.json</code> file at your repo root.
</p>
</div>
{/* Why worktrees */}
{/* Layout & workflow */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Why worktrees?</h2>
<h2 className="text-xl font-medium">Layout and workflow</h2>
<p className="text-white/60 leading-relaxed">
Without worktrees, running multiple agents on the same repo means they share the working
directory. One agent's changes interfere with another's. You can't safely run parallel
tasks.
</p>
<p className="text-white/60 leading-relaxed">
With worktrees, each agent gets its own directory and branch. They can work simultaneously
without conflict. When an agent finishes, you review the diff, merge the branch, and
archive the worktree.
</p>
</section>
{/* Directory structure */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Directory structure</h2>
<p className="text-white/60 leading-relaxed">
Paseo creates worktrees under <code className="font-mono">$PASEO_HOME/worktrees/</code>,
organized by a short hash of the source checkout path:
Worktrees live under <code className="font-mono">$PASEO_HOME/worktrees/</code>, grouped by
a hash of the source checkout path. Each worktree gets a random slug; the branch name is
chosen when you first launch an agent.
</p>
<Code>
<pre className="text-white/80">{`~/.paseo/worktrees/
├── 1vnnm9k3/
│ ├── tidy-fox/ # random slug
│ └── bold-owl/ # random slug
└── 4k8q2d1p/
└── swift-hare/ # random slug`}</pre>
└── 1vnnm9k3/ # hash of source checkout path
├── tidy-fox/ # worktree slug (branch set on first agent)
└── bold-owl/`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
The hash avoids collisions between repositories that share the same directory or remote
name. Worktree directory names are random slugs — the branch name is separate and chosen
when you first launch an agent in the worktree.
</p>
</section>
{/* Branches */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Branches</h2>
<p className="text-white/60 leading-relaxed">
When you create a worktree, Paseo generates a random directory name. The branch name is
set when you first launch an agent — Paseo generates one automatically.
</p>
<p className="text-white/60 leading-relaxed">
This means the worktree directory and branch are independent. You can rename the branch
later without affecting the worktree path.
</p>
</section>
{/* Multiple agents */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Multiple agents per worktree</h2>
<p className="text-white/60 leading-relaxed">
You can launch multiple agents into the same worktree. They share the working directory
and branch, which is useful when you want agents to collaborate on the same feature or
when one agent hands off to another.
</p>
<p className="text-white/60 leading-relaxed">
Be mindful of conflicts — agents working on the same files simultaneously can step on each
other. This works best when agents have distinct responsibilities or run sequentially.
</p>
<ol className="text-white/60 space-y-2 list-decimal list-inside">
<li>Create a worktree — Paseo runs your setup hooks</li>
<li>Launch an agent — a branch is created or assigned</li>
<li>Review the diff against the base branch</li>
<li>Merge or archive — archive runs teardown and removes the directory</li>
</ol>
</section>
{/* paseo.json */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Lifecycle hooks with paseo.json</h2>
<h2 className="text-xl font-medium">paseo.json</h2>
<p className="text-white/60 leading-relaxed">
When Paseo creates a worktree, it's a fresh checkout. Dependencies aren't installed,
config files aren't copied. You can automate setup by creating a{" "}
<code className="font-mono">paseo.json</code> file in your repository root:
Drop a <code className="font-mono">paseo.json</code> in your repo root. Paseo reads it
from the committed version of the base branch you picked, so uncommitted changes in other
branches don't apply.
</p>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": ["npm ci"],
"teardown": ["rm -rf .cache"]
},
"scripts": {
"test": { "command": "npm test" },
"web": { "command": "npm run dev", "type": "service", "port": 3000 }
}
}`}</pre>
</Code>
</section>
{/* Setup & teardown */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Setup and teardown</h2>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">setup</code> runs once after the worktree is created. A fresh
worktree has no installed dependencies and no ignored files (like{" "}
<code className="font-mono">.env</code>), so use setup to install and copy what you need.
<code className="font-mono">teardown</code> runs during archive, before the directory is
removed.
</p>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": [
"npm ci",
"cp \\"$PASEO_SOURCE_CHECKOUT_PATH/.env\\" \\"$PASEO_WORKTREE_PATH/.env\\""
"cp \\"$PASEO_SOURCE_CHECKOUT_PATH/.env\\" .env",
"npm run db:migrate"
],
"teardown": [
"npm run db:drop || true"
]
}
}`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
The <code className="font-mono">setup</code> array contains shell commands that run after
the worktree is created. Use it to install dependencies, copy local config files, or run
any other initialization.
Commands run with the worktree as <code className="font-mono">cwd</code>. Use{" "}
<code className="font-mono">$PASEO_SOURCE_CHECKOUT_PATH</code> to reach files in the
original checkout (untracked config, local caches, etc).
</p>
</section>
{/* Scripts & services */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Scripts and services</h2>
<p className="text-white/60 leading-relaxed">
You can also add a <code className="font-mono">teardown</code> array for cleanup commands
that run before Paseo removes the worktree directory during archive:
<code className="font-mono">scripts</code> are named commands you can run inside a
worktree on demand. Mark one as a <em>service</em> and Paseo supervises it as a
long-running process, assigns it a port, and routes HTTP traffic to it through the
daemon's reverse proxy.
</p>
<h3 className="text-lg font-medium mt-4">Plain scripts</h3>
<Code>
<pre className="text-white/80">{`{
"scripts": {
"test": { "command": "npm test" },
"lint": { "command": "npm run lint" },
"generate": { "command": "npm run codegen" }
}
}`}</pre>
</Code>
<h3 className="text-lg font-medium mt-6">Services</h3>
<Code>
<pre className="text-white/80">{`{
"scripts": {
"web": {
"type": "service",
"command": "npm run dev -- --port $PASEO_PORT",
"port": 3000
},
"api": {
"type": "service",
"command": "npm run api -- --port $PASEO_PORT"
}
}
}`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Omit <code className="font-mono">port</code> to let Paseo auto-assign one. Bind your
process to <code className="font-mono">$PASEO_PORT</code> rather than hard-coding — each
worktree gets a distinct port so multiple copies of the same service coexist.
</p>
<h3 className="text-lg font-medium mt-6">Reverse proxy</h3>
<p className="text-white/60 leading-relaxed">
Every service is reachable through the daemon at a deterministic hostname:
</p>
<Code>
<pre className="text-white/80">{`http://<script>.<branch>.<project>.localhost:<daemon-port>
# on the default branch, the branch label is dropped:
http://<script>.<project>.localhost:<daemon-port>`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">*.localhost</code> resolves to{" "}
<code className="font-mono">127.0.0.1</code> on modern systems, so these URLs work out of
the box. The proxy supports WebSocket upgrades.
</p>
<h3 className="text-lg font-medium mt-6">Service-to-service</h3>
<p className="text-white/60 leading-relaxed">
Services launched from the same workspace see each other's ports and proxy URLs. Given{" "}
<code className="font-mono">web</code> and <code className="font-mono">api</code> above,
each process gets:
</p>
<Code>
<pre className="text-white/80">{`PASEO_PORT=3000 # this service's port
PASEO_URL=http://web.my-app.localhost:6767 # this service's proxy URL
PASEO_SERVICE_API_PORT=51732
PASEO_SERVICE_API_URL=http://api.my-app.localhost:6767
PASEO_SERVICE_WEB_PORT=3000
PASEO_SERVICE_WEB_URL=http://web.my-app.localhost:6767`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Script names are upper-cased and non-alphanumerics become{" "}
<code className="font-mono">_</code>. Point your frontend at{" "}
<code className="font-mono">$PASEO_SERVICE_API_URL</code> instead of hard-coding a port.
</p>
</section>
{/* Terminals */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Terminals</h2>
<p className="text-white/60 leading-relaxed">
Open terminals automatically when a worktree is created. Useful for tailing logs or
leaving a REPL ready to go.
</p>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"teardown": [
"pkill -f \\"vite --port $PASEO_WORKTREE_PORT\\" || true",
"rm -rf \\"$PASEO_WORKTREE_PATH/.cache\\""
"terminals": [
{ "name": "logs", "command": "tail -f dev.log" },
{ "name": "shell", "command": "bash" }
]
}
}`}</pre>
</Code>
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4 text-white/80">
<strong>Important:</strong> Setup commands come from{" "}
<code className="font-mono">paseo.json</code> in the selected base branch. If you pick{" "}
<code className="font-mono">main</code>, Paseo reads the committed file on{" "}
<code className="font-mono">main</code>. Local or uncommitted changes in another branch
are not used for that worktree.
</div>
</section>
{/* Environment variables */}
{/* Env vars */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Environment variables</h2>
<p className="text-white/60 leading-relaxed">
Setup and teardown commands have access to these environment variables:
Setup, teardown, scripts, and services all see:
</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<code className="font-mono">$PASEO_SOURCE_CHECKOUT_PATH</code> — your source checkout
path (original repository root)
<code className="font-mono">$PASEO_SOURCE_CHECKOUT_PATH</code> — the original repo root
</li>
<li>
<code className="font-mono">$PASEO_ROOT_PATH</code> — legacy alias of{" "}
<code className="font-mono">$PASEO_SOURCE_CHECKOUT_PATH</code>
<code className="font-mono">$PASEO_WORKTREE_PATH</code> — the worktree directory
</li>
<li>
<code className="font-mono">$PASEO_WORKTREE_PATH</code> — the new worktree directory
<code className="font-mono">$PASEO_BRANCH_NAME</code> — the worktree's branch
</li>
<li>
<code className="font-mono">$PASEO_BRANCH_NAME</code> — the branch name created
<code className="font-mono">$PASEO_WORKTREE_PORT</code> — legacy per-worktree port
(prefer <code className="font-mono">$PASEO_PORT</code> inside services)
</li>
</ul>
<p className="text-white/60 leading-relaxed">Services additionally get:</p>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<code className="font-mono">$PASEO_WORKTREE_PORT</code> — the worktree port, when
runtime metadata exists
<code className="font-mono">$PASEO_PORT</code> — this service's assigned port
</li>
<li>
<code className="font-mono">$PASEO_URL</code> — this service's proxy URL
</li>
<li>
<code className="font-mono">$PASEO_SERVICE_&lt;NAME&gt;_PORT</code> /{" "}
<code className="font-mono">_URL</code> — peer service ports and URLs
</li>
<li>
<code className="font-mono">$HOST</code> — <code className="font-mono">127.0.0.1</code>{" "}
for local-only daemons, <code className="font-mono">0.0.0.0</code> when the daemon binds
all interfaces
</li>
</ul>
<p className="text-white/60 leading-relaxed">
Use <code className="font-mono">$PASEO_SOURCE_CHECKOUT_PATH</code> to copy files that
shouldn't be in git (like <code className="font-mono">.env</code>) from your source
checkout to the worktree.
</p>
<p className="text-white/60 leading-relaxed">
<code className="font-mono">$PASEO_WORKTREE_PORT</code> is available when the worktree was
bootstrapped with a port. That makes it useful for both starting services in setup and
stopping them again in teardown.
</p>
</section>
{/* Teardown */}
{/* CLI */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Teardown</h2>
<p className="text-white/60 leading-relaxed">
Teardown runs during archive, before Paseo removes the worktree directory. Use it for
cleanup that needs access to the worktree path or its assigned port.
</p>
<p className="text-white/60 leading-relaxed">
Common uses include stopping dev servers on{" "}
<code className="font-mono">$PASEO_WORKTREE_PORT</code>, deleting generated files, or
deregistering services tied to that worktree.
</p>
<h2 className="text-xl font-medium">CLI</h2>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": [
"npm ci",
"nohup npm run dev -- --port $PASEO_WORKTREE_PORT > \\"$PASEO_WORKTREE_PATH/dev.log\\" 2>&1 &"
],
"teardown": [
"pkill -f \\"npm run dev -- --port $PASEO_WORKTREE_PORT\\" || true",
"rm -f \\"$PASEO_WORKTREE_PATH/dev.log\\""
]
}
}`}</pre>
<pre className="text-white/80">{`paseo run --worktree feature-auth --base main "implement auth"
paseo worktree ls
paseo worktree archive feature-auth`}</pre>
</Code>
</section>
{/* Common patterns */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Common patterns</h2>
<h3 className="text-lg font-medium mt-6">Node.js / npm</h3>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": ["npm ci"]
}
}`}</pre>
</Code>
<h3 className="text-lg font-medium mt-6">Python / Poetry</h3>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": ["poetry install"]
}
}`}</pre>
</Code>
<h3 className="text-lg font-medium mt-6">Copy environment files</h3>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": [
"npm ci",
"cp \\"$PASEO_SOURCE_CHECKOUT_PATH/.env\\" \\"$PASEO_WORKTREE_PATH/.env\\"",
"cp \\"$PASEO_SOURCE_CHECKOUT_PATH/.env.local\\" \\"$PASEO_WORKTREE_PATH/.env.local\\""
]
}
}`}</pre>
</Code>
<h3 className="text-lg font-medium mt-6">Run database migrations</h3>
<Code>
<pre className="text-white/80">{`{
"worktree": {
"setup": [
"npm ci",
"cp \\"$PASEO_SOURCE_CHECKOUT_PATH/.env\\" \\"$PASEO_WORKTREE_PATH/.env\\"",
"npm run db:migrate"
]
}
}`}</pre>
</Code>
</section>
{/* Workflow */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Workflow</h2>
<p className="text-white/60 leading-relaxed">The typical workflow is:</p>
<ol className="text-white/60 space-y-2 list-decimal list-inside">
<li>Create a worktree — Paseo creates the directory and runs setup</li>
<li>Launch an agent — Paseo creates or assigns a branch</li>
<li>Agent works in isolation — changes stay in its worktree</li>
<li>Review the diff — compare against the base branch</li>
<li>Merge or discard — if approved, merge the branch; otherwise archive</li>
<li>Archive the worktree — cleans up the directory and optionally the branch</li>
</ol>
<p className="text-white/60 leading-relaxed">
You can run multiple agents in different worktrees simultaneously. Each worktree has its
own branch and working directory.
</p>
</section>
{/* CLI reference */}
<section className="space-y-4">
<h2 className="text-xl font-medium">CLI reference</h2>
<p className="text-white/60 leading-relaxed">Create an agent in a new worktree:</p>
<Code>
<pre className="text-white/80">{`paseo run --worktree feature-auth --base main "implement auth"`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">List all worktrees:</p>
<Code>
<pre className="text-white/80">{`paseo worktree ls`}</pre>
</Code>
<p className="text-white/60 leading-relaxed">
Archive a worktree (stops agents, removes directory):
</p>
<Code>
<pre className="text-white/80">{`paseo worktree archive feature-auth`}</pre>
</Code>
</section>
{/* Metadata */}
<section className="space-y-4">
<h2 className="text-xl font-medium">Metadata</h2>
<p className="text-white/60 leading-relaxed">
Paseo stores metadata in each worktree's git directory to track the base branch. This is
used for diff operations and to know what branch to merge into.
</p>
<p className="text-white/60 leading-relaxed">
You don't need to manage this manually — Paseo handles it when creating and archiving
worktrees.
</p>
</section>
</div>
);
}