modules/worktrees.md
Parallel agents should use git worktrees rather than sharing one checkout.
The default for any code or active-doc edit is to work in an isolated worktree
with an allocated port.
Avoid visible sibling directories such as turf-monster-feature-name as the long-term default. They make /Users/alex/projects hard to scan at scale.
Preferred layout:
/Users/alex/projects/<repo> # primary checkout
/Users/alex/projects/<repo>/.worktrees/<task-slug>
Do not include an agent id in the path. Tasks can transfer between agents, and multiple agents may collaborate on one branch.
Think of worktrees as desks and primary checkouts as loading docks. Agents do
feature work at desks. The primary checkout stays stable for reading,
integration, final merge, and deploy.
When a new agent session starts actual implementation work:
Task.slug is immutable and generated by the app;
use metadata["devops"]["worktree_slug"] for the human-readable feature
handle. The task should include acceptance criteria, affected repos, risk
tags, expected test_plan, and release-slug metadata when relevant.bin/agent-worktree plan <app> <task-slug>.bin/agent-worktree new <app> <task-slug>.bin/agent-worktree bind-task <app> <task-slug> <task-record-slug-or-url>
so whereami, terminal context, snapshots, and PR bodies can lead from the
task record.building.bin/agent-worktree up <app> <task-slug> when a browser or local URL is
needed./Users/alex/projects/<repo>/.worktrees/<task-slug>.building and mark it for operator validation: bin/task update <task-slug> --local-url http://localhost:<port>/<path> --approval waiting
In chat, return the URL in this exact top-level format:
Task: https://mcritchie.studio/tasks/<task-slug>
Local Demo: http://localhost:<port>/<path>
Local Inbox: http://localhost:<port>/_studio/local_emails # only for email/auth flows
Waiting approval cards float to the top of their stage and pulse on the board.
After approval, set --approval approved; after requested changes, set
--approval changes_requested and keep building.
11. Commit coherent work on the feature branch.
12. Run bin/agent-worktree finish <app> <task-slug> to produce the PR/QA
packet.
13. Update the task with branch, PR URL, local URL, checks_run, and any
changed acceptance criteria. Add a task conversation handoff note with the
change summary, verification, and review focus. Move it to submitted when
the PR is ready for Avi.
14. Return the task URL first, then the PR URL, branch, worktree path, local
URL, tests, and PR/QA recommendation in the handoff. Do not merge to main
unless assigned the QA/Release lane.
Exceptions:
Use McRitchie Studio's launcher for new parallel task stacks:
cd /Users/alex/projects/mcritchie-studio
bin/agent-worktree apps
bin/agent-worktree plan turf-monster docs-stack
bin/agent-worktree new turf-monster docs-stack
bin/agent-worktree bind-task turf-monster docs-stack task-abc123def456
bin/agent-worktree up turf-monster docs-stack
bin/agent-worktree finish turf-monster docs-stack
The launcher creates /Users/alex/projects/<repo>/.worktrees/<task-slug>, branches from the current base ref — origin/release when the repo has one (the persistent feature-PR target), else origin/main for repos that have not run bin/release init — copies the primary .env, writes .env.agent-stack, prepares the isolated database, and prints the local URL.
Use bin/agent-worktree status <app> <task-slug> to recover the URL later, and bin/agent-worktree down <app> <task-slug> to stop a running stack.
Use bin/agent-worktree finish <app> <task-slug> when the work is committed
and ready for PR/QA handoff.
Use the launcher as the source of truth for worktree stack state:
bin/agent-worktree list
bin/agent-worktree whereami
bin/agent-worktree bind-task turf-monster task-slug task-abc123def456
bin/agent-worktree shell-hook zsh
bin/agent-worktree status turf-monster task-slug
bin/agent-worktree finish turf-monster task-slug
bin/agent-worktree doctor
bin/agent-worktree snapshot
bin/agent-worktree cleanup
bin/agent-worktree cleanup --reclaim
bin/agent-worktree cleanup --reclaim --yes
bin/agent-worktree remove turf-monster task-slug --yes
bin/agent-worktree scale status
list shows task, health, URL, branch, dirty state, merge state, ahead/behind, database, Redis DB, pidfile state, and local inbox URL.bind-task stores the production McRitchie Studio task record slug and URL in
the generated stack env, then refreshes .agent-context.json. Pass either
task-<hex> or https://mcritchie.studio/tasks/task-<hex>. Use this after
the production task exists and before PR handoff so the terminal context and
PR body lead from the task record.whereami reads the nearest .agent-context.json marker and prints the app,
production task record, task URL, task/worktree slug, branch, local URL, port,
Redis DB, database, terminal title, prompt badge, and shell exports. Pass
<app> <task-slug> to refresh the marker for a known stack, --json for
machine-readable output, or --shell for exports/title commands. The shell
form recomputes exports from validated scalar context fields at runtime and
ignores any persisted executable shell content in .agent-context.json.shell-hook zsh prints a zsh hook that refreshes AGENT_CONTEXT_*
variables and the terminal title whenever the prompt redraws or the working
directory changes. It does not edit shell dotfiles; source it explicitly when
you want evergreen terminal titles.status shows the detailed state for one generated stack.finish prints a feature graduation packet and PR body. It blocks dirty
worktrees, branches with no commits ahead of the base ref, stale branches
behind the base ref, and already-merged branches. Add --push to push the
branch. --push --pr additionally requires a bound production task record
from bind-task, then creates a draft PR based on the base branch
(release, else main) through gh when available.doctor reports lifecycle drift such as missing stack env files, reused ports, reused Redis DBs, stale pidfiles, dirty worktrees, disabled local email capture, and clean branches already merged to the base ref. It also reconciles git worktree list against the managed registry per repo and flags any orphan — a git worktree that is neither the primary checkout nor a managed .worktrees/* dir — with its path, branch, and merge/clean state. An orphan whose directory was deleted on disk but is still tracked by git is reported distinctly as prunable (clear it with git -C <repo> worktree prune); doctor and snapshot --write both tolerate it and still exit 0. Orphans are detect-and-report only; removal stays approval-gated (bin/agent-worktree remove).snapshot prints a non-secret JSON registry of every generated worktree,
including health, local URLs, branch state, Redis DB, database name, cleanup
candidacy, compare URL, and doctor issues. The payload also carries a
top-level capacity block (floor, step, current, used, free,
physical_max) describing the elastic Redis band.snapshot --write writes the same registry to
/Users/alex/projects/.agents/worktree-registry.json for conductor sessions,
dashboards, and future automation. Set
AGENT_WORKTREE_REGISTRY=/tmp/worktree-registry.json when a sandboxed
session needs a scratch write instead of the shared projects registry.cleanup is a dry run. It prints clean worktree candidates whose branch is
either contained in the base ref (origin/release, else origin/main) or has
an empty final diff against the base ref after a squash merge. A branch merged
into release but not yet shipped to main therefore counts as done. Each
candidate prints the exact safety class (merged or base-equivalent), base
ref, ahead/behind count, stack health, /up code, pidfile state, Redis DB,
database state, and the exact bin/agent-worktree remove … --yes command. Use
that dry run as the approval packet before deleting anything.cleanup --write appends candidates to ../maintenance/delete-later.md. It does not remove files, worktrees, branches, databases, Redis keys, or processes.cleanup --reclaim is the scale-down-on-close normal flow: a merged
worktree self-releases its Redis slot the same way a stack scales down when it
closes. The dry run (no --yes) lists only the worktrees that are SAFE to
auto-remove — clean and either contained in the base ref or
base-equivalent (the same cleanup_ready? criteria as cleanup) — and prints
the same safety evidence and removal command as cleanup. It never lists a
dirty or unmerged worktree, and the candidate set is sourced from
.worktrees/* only, so the primary checkout is never a candidate.cleanup --reclaim --yes runs the same full teardown as remove for each
safe candidate (stop the stack, flush the stack's Redis DB, update the cleanup
ledger, remove the Git worktree, delete the stale local branch), re-verifying
each candidate under the worktree lock so one that turned dirty/unmerged in the
interim is skipped. After the batch it shrinks the Redis band toward the floor
(maybe_scale_in) and refreshes the registry once. Output names each reclaimed
worktree, the freed Redis DB, and the resulting band size. Safe to re-run; with
no candidates it prints a clear no-op message and changes nothing.remove <app> <task-slug> --yes is the approved deletion path after Mr.
McRitchie or the conductor authorizes cleanup. It refuses dirty or
non-equivalent worktrees, stops the stack, flushes the stack's Redis DB (so a
reused DB number cannot inherit stale keys), updates the cleanup ledger, removes
the Git worktree, deletes the stale local branch, shrinks the Redis band toward
the floor when slots free up, and refreshes the registry.scale status prints the Redis band: floor, step, current band + DB range,
used, free, and the physical ceiling (databases from Redis). scale out /
scale in are manual nudges (respect floor and physical ceiling). scale
--provision [--yes] raises the physical databases and restarts Redis once;
see Scale Note.Deletion remains approval-gated:
bin/agent-worktree cleanup <app> to see candidates.bin/agent-worktree doctor <app> has no dirty or unique-work
warnings for the target.bin/agent-worktree remove <app> <task-slug> --yes.bin/qa-intake --refresh --apps <apps> so the conductor view no longer
reports the removed worktree.GitHub squash merges do not preserve the feature branch SHA on the base. After a
PR lands, a branch can appear behind its base ref even when all of its content
was merged. Do not rely on ahead/behind alone.
The launcher now treats an empty final diff against the base ref
(origin/release, else origin/main) as a cleanup candidate. Before removing a
squash-merged worktree manually (substitute origin/main for repos without a
release branch):
origin/release is current. git diff --stat origin/release..HEAD
git diff --name-status origin/release..HEAD
bin/agent-worktree remove <app> <task-slug> --yes after approval.release
branch, not main). This is now the launcher default: bin/agent-worktree new
cuts from origin/release, finish --pr opens the PR with --base release,
and ahead/behind + cleanup/merge checks all reckon against origin/release.
bin/release init has no
origin/release, so the launcher transparently falls back to origin/main
for the branch cut, the PR base, and the comparison base. The resolved base
is reported per-worktree (base_ref in snapshot/registry, the base: line
in finish). bin/release merge still rejects any PR whose base isn't
release, so a stray main-based PR is caught at merge time.main checkout unless you are the
explicit deploy owner for that repo.release is the
reviewed integration lane (main is the shipped lane, fast-forwarded from
release at ship time), not a place to rush code so it is not lost.release. Avi or
the designated release conductor owns merging to release (and shipping
release → main).../maintenance/delete-later.md before deleting them.When several agents build in parallel and their work converges on one branch
— e.g. scaffolding a new app: Rolio's first cut was 4 gap features by 4 agents
merged together — isolation and merge discipline matter more than usual. The
patterns below came out of that run.
Isolation — use a manual git worktree add per agent.
isolation: worktree mode was unreliable when the target
repo differs from the session repo — agents leaked their edits onto the main
checkout instead of an isolated worktree. For cross-repo or multi-agent fan-out,
give each agent an explicit git worktree add -b <branch> .worktrees/<slug>
<base> and point it at that absolute path.Design the work to merge cleanly — new-files-first.
<%= render %> line at a named
anchor. Each agent adds only its one render line at the agreed anchor; the
body lives in the agent's own partial. Conflicts shrink to one predictable line.namespace/scope of its own) rather than editing a shared resource line.schema.rb. Either nominate a single migration
owner, or have the orchestrator pre-add the columns before fanning out so
the agents only read them.The recurring conflict: the CSS end-of-file seam.
Merge sequentially, suite green between merges.
This is the multi-agent build path referenced by
../system/new-app-onboarding-sop.md.
A feature-agent handoff should include:
bin/qa-intake as a
substitute for the task-board record.qa_feedback remains open in practice,
and the latest handoff note the feature agent added.devops.checks_run.bin/agent-worktree finish result.Do not leave Mr. McRitchie with "run these commands." Start the stack, prove the
URL, and name any blocker that truly needs owner action.
bin/agent-worktree snapshot --write is the local registry for scale. It does
not contain secrets and is safe to hand to another agent session as current
machine context.
Use it when:
The registry is intentionally local under /Users/alex/projects/.agents/.
McRitchie Studio documents and owns the format, but the file itself reflects the
current machine and should not be treated as a Git-tracked source of truth.
If the agent runtime blocks writing to that directory, rerun the command with
filesystem approval or set AGENT_WORKTREE_REGISTRY to a writable scratch path.
The launcher writes a non-secret .agent-context.json file into every generated
worktree during new, and refreshes it during up, status, and
whereami <app> <task-slug>. The marker is added to the repository's local
Git exclude file so it does not show as untracked work or enter commits.
Use it from inside a generated worktree:
bin/agent-worktree whereami
bin/agent-worktree whereami --json
bin/agent-worktree whereami --shell
If the current directory is nested below the worktree root, call the launcher by
absolute path or from the shell PATH; whereami walks upward until it finds
the marker.
Bind the production task record once the task exists:
bin/agent-worktree bind-task <app> <task-slug> task-abc123def456
bin/agent-worktree bind-task <app> <task-slug> https://mcritchie.studio/tasks/task-abc123def456
After binding, the marker carries both the human-readable worktree slug and the
generated production task record. whereami --shell exports the task record as
AGENT_CONTEXT_TASK_RECORD, the browser URL as AGENT_CONTEXT_TASK_URL, and
the human slug as AGENT_CONTEXT_WORKTREE_SLUG. The prompt badge includes the
production task slug when available. The shell output is generated from scalar
marker fields each time; agents must not store or trust executable shell lines
inside .agent-context.json.
For an evergreen terminal title in zsh, source the generated hook:
eval "$(/Users/alex/projects/mcritchie-studio/bin/agent-worktree shell-hook zsh)"
The hook exports:
AGENT_CONTEXT_APPAGENT_CONTEXT_TASKAGENT_CONTEXT_WORKTREE_SLUGAGENT_CONTEXT_TASK_RECORDAGENT_CONTEXT_TASK_URLAGENT_CONTEXT_PORTAGENT_CONTEXT_URLAGENT_CONTEXT_TITLEAGENT_CONTEXT_BADGEThe hook updates the terminal title automatically. Shell prompt customization
can include $AGENT_CONTEXT_BADGE wherever the operator wants the badge to
appear.
Worktree servers must use a non-primary port from the app's reserved range. See ports-and-processes.md.
Worktree tooling should make parallel stacks "just work" without user terminal chores.
Each worktree stack needs its own:
3101, 3102, etc. for Turf Monster).9 (see Scale Note).DATABASE_URL.<app>_test_<slug>), provisioned by bin/agent-worktree new and pinned via TEST_DATABASE_URL in .env.test.local (see Running tests).APP_PORT so magic links point at the stack.LOCAL_EMAIL_CAPTURE=1 so mail is recorded locally instead of sent.Do not let two Sidekiq processes share one Redis DB while pointing at different databases. A job enqueued by one stack can be processed by the other stack and silently mutate the wrong records.
Worktree magic links are local-first through /_studio/local_emails. The central launcher writes LOCAL_EMAIL_CAPTURE=1, blanks provider mail credentials in .env.agent-stack, and prints the inbox URL next to the app URL. Agents should request the magic link in the UI, then open:
http://localhost:<port>/_studio/local_emails
The inbox shows recent outbox rows and proof links such as magic-link sign-in URLs. Worktree stacks should not email real recipients unless the task is specifically testing real delivery. For provider tests, intentionally set LOCAL_EMAIL_CAPTURE=0 and restore the needed mail credentials in that stack env.
Callback-heavy flows such as Stripe, Google OAuth, CDP/MoonPay, webhooks, and emailed magic links stay on the primary port unless the provider and local listener are configured for the worktree port.
bin/agent-worktree new provisions an isolated test database (<app>_test_<slug>) and writes .env.test.local with TEST_DATABASE_URL pointing at it. config/database.yml's test.url reads TEST_DATABASE_URL, and an explicit url: wins over DATABASE_URL — so a plain bin/rails test resolves to the isolated test DB out of the box:
bin/rails test # auto-resolves to <app>_test_<slug>; no manual prep
bin/agent-worktree test mcritchie-studio <slug> # same, single-process + hermetic env
Do not source .env.agent-stack before running tests. You do not need the dev DATABASE_URL to test (the test DB resolves on its own), and sourcing it sets AGENT_WORKTREE=1/LOCAL_EMAIL_CAPTURE=1, which routes mail into the local capture store and diverges email-delivery tests from CI. Source .env.agent-stack only for dev-DB chores like bin/rails runner seeding. bin/agent-worktree test sidesteps this with a hermetic env (correct Ruby PATH, RAILS_ENV=test, single-process to avoid the parallel worker-DB clone deadlock on a cold test DB).
Redis capacity has two layers:
databases setting. It is fixed at Redis
startup; changing it needs a restart. Stock Redis exposes 0-15 (16 DBs).9. It is elastic and restart-free within the physical ceiling.The band idles at 20 slots (FLOOR) and changes by 10 (STEP):
allocate_redis_db grows the band by 10 (scaled out: 20 -> 30 slots) and
retries. No restart. At the physical ceiling it aborts with guidance to run
cleanup or scale --provision.remove, cleanup --write, and cleanup --reclaim --yes
drop the band by 10 (never below the floor, never stranding a still-used DB) as
slots free up (scaled in: 30 -> 20 slots). No restart. cleanup --reclaim
--yes is the hands-off scale-down-on-close path: it releases every safe
candidate, then calls maybe_scale_in once for the batch.The band size is persisted in /Users/alex/projects/.agents/redis-capacity.json
and band allocation + capacity mutation are guarded by a flock on
/Users/alex/projects/.agents/agent-worktree.lock so concurrent new/up
runs cannot collide.
Inspect the band with bin/agent-worktree scale status. To realize the full
20-slot floor you need physical databases >= 29 (DB 9 band start + 20). The
band caps band hand-outs at the physical ceiling, so on stock Redis (16 DBs)
only DBs 9-15 are usable until you provision.
To raise physical capacity (one-time, target databases 64):
bin/agent-worktree scale --provision # interactive confirm
bin/agent-worktree scale --provision --yes # skip the prompt
This edits the brew redis.conf ($(brew --prefix)/etc/redis.conf, overridable
with AGENT_REDIS_CONF) and restarts Redis exactly once. It is idempotent
(no-op when databases is already at/above target). The restart bounces every
running worktree stack on localhost:6379, so it belongs to the QA/infra lane
during a quiet window, never mid-session while other stacks are live.
Overrides: AGENT_REDIS_FLOOR, AGENT_REDIS_STEP,
AGENT_REDIS_PHYSICAL_TARGET, and the legacy AGENT_REDIS_MAX_DB (pins the band
top explicitly). Do not set band overrides past what Redis actually serves.
We emailed a one-tap sign-in link to . It expires shortly and can only be used once.
No email? Check spam, or close this and try again.