Clone-on-demand worktrees (Phase 299.1)
A heavy alternative to cinatra setup branch that gives a git worktree its own full
Postgres database (not just a schema). Created dormant; cinatra clone start
brings up a per-clone WayFlow (+ optional Tailscale Funnel) on demand.
Two isolation paths
Section titled “Two isolation paths”cinatra setup branch (light) | cinatra setup clone (heavy) | |
|---|---|---|
| What is isolated | A Postgres schema (cinatra_<slug>) inside the shared postgres DB | A separate Postgres database (cinatra_clone_<slug>) |
Better Auth (public.user / session / …) | shared with main | isolated per clone (own public) |
| Teardown | DROP SCHEMA … CASCADE (may miss edge cases) | DROP DATABASE … WITH (FORCE) (clean) |
| Snapshot mechanism | row-by-row INSERT … ON CONFLICT from the live cinatra schema | CREATE DATABASE … TEMPLATE cinatra_seed (file-level copy; preserves sequences/identity/indexes/constraints) |
| Port band | 3001-3099 (Next.js) | 3100-3119 (Next.js) + 3200-3219 (WayFlow) |
BULLMQ_QUEUE_NAME | cinatra-bg-<slug> | cinatra-clone-<slug> |
| Worktree creation | Claude Code EnterWorktree → .claude/worktrees/worktree-<name> | cinatra setup clone owns it → ../cinatra-ai-<slug>, branch cinatra-ai-<slug> |
| Operator action | pnpm cinatra setup branch (auto via hook) | pnpm cinatra clone refresh-seed once, then pnpm cinatra setup clone <name> |
| Deps install | manual pnpm install | automatic (corepack pnpm install) on creation |
299.1 constraint: both
clone refresh-seedandsetup clonerequire the sourceSUPABASE_SCHEMAto be unset or exactly"cinatra"— they throw clearly otherwise. Worktrees on a custom app schema stay onsetup branchuntil a later phase widens this.
Reach for setup clone when a worktree needs true Better-Auth-table isolation, or a
real point-in-time snapshot of the live DB (e.g. for testing a destructive migration), or
the later 299.x slices’ dedicated WayFlow + public tunnel. Stick with setup branch for
ordinary code-level work — it’s lighter, hook-driven, and shares main’s Better Auth state.
The four commands
Section titled “The four commands”cinatra clone refresh-seed [--source-env <path>]
Section titled “cinatra clone refresh-seed [--source-env <path>]”(Re)builds the cinatra_seed template database — the source every clone forks from.
- Connects to the maintenance DB (forced to
/postgres) —CREATE/DROP/ALTER DATABASEnever runs while connected to the DB being mutated. - Drops any existing
cinatra_seed(clearsIS_TEMPLATE+ALLOW_CONNECTIONS, terminates backends, drops). - Creates an empty
cinatra_seed. pg_dumpthe live app DB (schemaspublic+cinatraonly — branch worktreecinatra_<slug>schemas are explicitly excluded),--clean --if-exists --no-owner --no-privileges --format=plain, thenpsqlrestore intocinatra_seed. Uses the sharedrunPostgresCommandhelper — hostpg_dump/psqlif available, falling back to the pinnedpostgres:17-alpinedocker client image. The fallback’s host-reachability rewrite is platform-specific: macOS rewrites127.0.0.1/localhostin the connection string tohost.docker.internal; Linux keeps127.0.0.1and adds--network hostto thedocker runinvocation.TRUNCATEevery table inSEED_SKIP_TABLES(the operational set:agent_runs,agent_run_messages,audit_events,notifications,traces,chat_threads,record_activities,planned_actions,review_tasks,usage_events).TRUNCATEevery table inSEED_AUTH_SCRUB_TABLES(session,verification,oauthAccessToken,oauthRefreshToken,oauthConsent) — mixed-case identifiers, withquoteIdentifierfor both schema and table. The Better Auth identity tables (user,account,organization, …) are intentionally kept so the operator can sign into a clone with the same credentials.- Records
clone_seed_info(source DB, refresh timestamp, git SHA) incinatra.metadata. ALTER DATABASE cinatra_seed WITH IS_TEMPLATE true ALLOW_CONNECTIONS falsesoCREATE DATABASE … TEMPLATE cinatra_seedalways succeeds.
cinatra setup clone [<name>] [--slug <name>] [--worktree-path <path>] [--source-env <path>] [--force]
Section titled “cinatra setup clone [<name>] [--slug <name>] [--worktree-path <path>] [--source-env <path>] [--force]”Creates and provisions a dormant deep-fork clone. Two modes:
- CLI-owned (recommended) —
cinatra setup clone <name>(positional) or--slug <name>. The CLI creates the git worktree itself: derives the main repo root (viagit rev-parse --git-common-dir), thengit -C <mainRepoRoot> fetch origin+git worktree add <parentDir>/cinatra-ai-<slug> -b cinatra-ai-<slug> origin/main(idempotent — reuses an existingcinatra-ai-<slug>branch without-b; skips creation if a worktree is already registered to the slug). NoEnterWorktree, no.claude/worktrees/, noworktree-prefix. After provisioning it auto-runscorepack pnpm installin the new worktree (only when the worktree was just created ornode_modulesis absent; usescorepackso the pinnedpackageManageris honored — a stray global pnpm mangles the lockfile). An install failure is loud and exits non-zero but never rolls back the DB/worktree (provisioning already succeeded). - Legacy / back-compat (no name) —
cinatra setup clonewith no slug provisions the current worktree in place (slug fromresolveRealBranchName,cinatra-ai-/worktree-prefixes stripped). This is the path the Phase 299.4EnterWorktreehook uses (see “Hook-driven provisioning” below) — it does NOT create or relocate a worktree.
Internal slug stays short: cloneSlugFromBranch strips a leading cinatra-ai-
or worktree- segment, so the DB is cinatra_clone_<slug> regardless of the
branch/dir prefix.
Provisioning steps (both modes):
- Resolves the slug (positional/
--slug, else fromresolveRealBranchName(worktreePath), detached-HEAD safe) and derivesdbName = cloneDbName(slug)(cinatra_clone_<slug-with-underscores>). - Verifies
cinatra_seedexists and is a template — errors with a clearrun: cinatra clone refresh-seedhint otherwise. - Allocates the registry slot under a file lock (
~/.cinatra/clones.json.lock, inode-ownership aware so a concurrent stale-steal cannot kill a fresh lock). Lowest free index 0-19; throws on cross-worktree slug aliasing. Writes the slot in stateprovisioning. - Strict-compares any existing
.env.localagainst every clone-derived value (CINATRA_CLONE_SLUG, theSUPABASE_DB_URLdatabase path,SUPABASE_SCHEMA=cinatra,PORT,BULLMQ_QUEUE_NAME,BETTER_AUTH_URL,NEXT_PUBLIC_*). Mismatch aborts unless--force. (Phase 299.2 droppedLANGGRAPH_CINATRA_BASE_URLfrom the compare — LangGraph was retired in Phase 175 and no runtime code reads it.) CREATE DATABASE cinatra_clone_<slug> TEMPLATE cinatra_seed(idempotent — kept if it already exists).- Writes the worktree
.env.local(mode 0600). - Flips the slot from
provisioningtoreadyunder the file lock — a failure between steps 3 and 7 leaves the slotprovisioning, recoverable by a re-run.
cinatra clone prune [--worktree-path <path>] [--slug <slug>] --yes
Section titled “cinatra clone prune [--worktree-path <path>] [--slug <slug>] --yes”Destroys a clone. Order is validate-registry-first — a malformed registry throws before any DROP runs.
requireUsableRegistry(refuses malformed — leaves the file intact for repair).- Derives
dbName = cloneDbName(slug)deterministically (never trusted from a stored value); a mismatchedslot.dbNameaborts with a registry-inconsistent error. isProtectedDbName(dbName)hard guard — fail closed againstpostgres/cinatra/cinatra_seed/template*/ anything not shaped like a clone DB.- Connects to the maintenance DB (
/postgres), terminates backends ondbName,DROP DATABASE IF EXISTS … WITH (FORCE). - Redis queue cleanup for
bull:cinatra-clone-<slug>:*. - Releases the registry slot only if the cleanup succeeded — otherwise retains the slot with a re-run hint so orphaned keys are never silently abandoned.
- CLI-owned worktree removal (
pruneCliOwnedWorktree): a 3-layer guard — recordedworktreePath≠ main repo root, equals the expected<parentDir>/cinatra-ai-<slug>sibling, and is listed ingit worktree list --porcelain— thengit worktree remove(→--forceretry) andgit branch -D cinatra-ai-<slug>so teardown is zero-residue. A legacy / non-CLI-owned path (e.g. a.claude/worktrees/...EnterWorktree clone) is skipped entirely — DB/slot/Redis-only behavior is preserved for it. The same guard is applied per-slot byprune --stale.
cinatra clone list
Section titled “cinatra clone list”Read-only registry table (slug, ports, database, state, worktree, createdAt). Tolerates a malformed registry by reporting corruption rather than crashing.
The clone registry
Section titled “The clone registry”~/.cinatra/clones.json is the source of truth for port allocation. Dormant clones
hold no listening socket, so findFreePort cannot see them — only the registry can. Pure
module: packages/cli/src/clone-registry.mjs.
Shape:
{ "version": 1, "clones": { "<slug>": { "index": 0, "nextjsPort": 3100, "wayflowPort": 3200, "dbName": "cinatra_clone_<slug-with-underscores>", "worktreePath": "/abs/path", "state": "provisioning" | "ready", "createdAt": "<ISO 8601>" } }}readRegistry deep-validates every slot (index in 0..19, ports match the index, dbName
matches cloneDbName(slug), state is in provisioning | ready, etc.). A
syntactically-valid-but-shape-invalid registry is classified malformed —
requireUsableRegistry throws so a corrupt file cannot drive a colliding port or a
wrong-DB destructive op. The bad file is left in place for the operator to repair.
Known limitations
Section titled “Known limitations”- Wrong-version host
pg_dump—runPostgresCommandfalls back to the docker client image only when the host binary is absent. A wrong-version hostpg_dumpexits nonzero and surfaces a clear error; that matches the 3 existingcinatra backupcallers. Changing the fallback to also trigger on nonzero exit is a behavior change to shared shipped code — routed as a separate follow-up. clone prunewith no reachableredis-cli—cleanupRedisQueueKeysresolves a runner: (1) hostredis-cli(authoritative for any URL incl. remote, passes-u); else, only for a loopback Redis, (2)docker execinto the single running container that publishes the configured Redis port (matched as<hostport>->6379/tcp— Redis’ standard internal port; a non-standard internal port simply won’t match → fail closed). Container identity is the published port, never a name guess — 0 or >1 candidates fail closed so prune can never DEL the wrong project’s keys or falsely release the slot. A remoteREDIS_URLwith no hostredis-clialso fails closed. On failure the registry slot is retained per the deliberate prune-on-Redis-failure design (codex r1 finding B1, preserved in 299.1) — the DB is already dropped; make Redis reachable and re-run prune (idempotent) to release the slot.
Lifecycle commands (Phase 299.2 / 299.3)
Section titled “Lifecycle commands (Phase 299.2 / 299.3)”cinatra clone start [<slug>] [--rebuild-wayflow] [--tailscale-host-network]
Section titled “cinatra clone start [<slug>] [--rebuild-wayflow] [--tailscale-host-network]”Brings up the local stack:
- Acquires the per-clone runtime lock (
~/.cinatra/clones/<slug>/clone.lock). - Validates
TS_AUTHKEYif set in env (rejects malformed). The flag form--tailscale-authkeyis intentionally REJECTED — pass via env to keep the secret out of shell history andps. - Verifies the clone DB is reachable (
SELECT 1oncinatra_clone_<slug>). - Builds
cinatra-wayflow:localif the image is missing (--rebuild-wayflowforces rebuild on every start). - Renders the compose template into
~/.cinatra/clones/<slug>/compose.yml. - Generates/reads a per-clone
CINATRA_BRIDGE_TOKENfromcinatra.metadata['bridge_token']in the clone DB (re-rolled once on first start; clones never inherit main’s bridge token). - Spawns
pnpm dev(cwd = worktree, detached, new process group). Writesnextjs.pid+nextjs.log(truncated). Skipped on idempotent re-entry when pid + cwd +/api/healthall check out. docker compose -p cinatra-clone-<slug>-<slot> up -d wayflow [tailscale]. Stdout/stderr scrubbed of anyTS_AUTHKEYcontent if Tailscale enabled.- Health-polls
http://localhost:31NN/api/health+http://localhost:32NN/.healthup to 60s each. - If
TS_AUTHKEYwas set: pollstailscale status --jsoninside the sidecar forSelf.DNSName; the Funnel URL is read from there (never synthesised from hostname+tailnet). Probes<funnel-url>/api/mcp/healthfor 200 then UPSERTsconnector_config:mcp_server.publicBaseUrlinto the clone DB. - Without
TS_AUTHKEY: clone runs local-only.publicBaseUrlstays cleared. The notice is printed so operators know.
cinatra clone stop [<slug>]
Section titled “cinatra clone stop [<slug>]”- Clears
mcp_server.publicBaseUrlin the clone DB (no stale Funnel URL across stop/start). docker compose -p ... down.- SIGTERMs the Next.js process group (verifies cwd-match first; a stale pid reused by another process is never signalled).
- SIGKILLs after a 10s grace if still alive.
- Removes
nextjs.pid.
The clone DB + registry slot survive. To drop the clone DB entirely, run
cinatra clone prune --slug <s> --yes.
cinatra clone status [<slug>]
Section titled “cinatra clone status [<slug>]”Read-only diagnostic — pid alive Y/N, /api/health reachable Y/N, WayFlow
/.health reachable Y/N, compose project name, runtime-lock state, log path.
cinatra dev tunnel <start|stop|status> (Phase 299.6)
Section titled “cinatra dev tunnel <start|stop|status> (Phase 299.6)”The bare pnpm dev MAIN-instance equivalent of the per-clone Funnel
(cinatra clone start, Phase 299.3). Both share the single deterministic
deriveDevTailscaleHostname({dbUrl, schema}) source of truth — the SAME
deriver the app’s dev-tab flyout preview uses — so the predicted hostnames
never collide: main → cinatra-main, heavy clone → cinatra-clone-<slug>,
light worktree → cinatra-<slug>.
cinatra dev tunnel start
Section titled “cinatra dev tunnel start”- Dev-only HARD REFUSAL. Exits with a thrown error (exit 1) unless
CINATRA_RUNTIME_MODE=development, BEFORE any Docker / Nango / DB side effect (threat T-2996-08, dev-only-boot-path convention). The message tells the operator that production main must instead use the operator-supplied public URL at/administration/development?tab=tunnel. - Collision guard. Asserts no real registered clone has claimed the
reserved name
dev-mainbefore any side effect (throws clearly if one exists). - Uses the reserved fixed slug
"dev-main"(index 0) fed into the slug-parameterized PURE clone-runtime path builders (cloneComposePath/cloneTailscaleServePath/cloneComposeProjectName), deliberately bypassingloadReadyCloneSlot/ the clone registry. - Predicts hostname
cinatra-main(e.g.https://cinatra-main.taild5286c.ts.net) viaderiveDevTailscaleHostname. - Next.js port is the bare
pnpm devdefault (3000 /env.PORT), NOT the 3100+ clone band. - Idempotent — skips if the dev-main compose project is already up.
cinatra dev tunnel stop
Section titled “cinatra dev tunnel stop”- Tears the dev-main Tailscale sidecar down.
- MR-01 source-guarded: reads
publicBaseUrlSourcefrom the main DB and clearspublicBaseUrlONLY when this subsystem owns it (source ∈ {tailscale-auto, tailscale-funnel}). An operator-set manual URL is left UNTOUCHED.
cinatra dev tunnel status
Section titled “cinatra dev tunnel status”Read-only — reports predicted hostname vs registered hostname + whether
publicBaseUrl is currently set in the main DB. Never throws on “not
running” (clean output, short non-fatal probe). Correctly distinguishes the
new deterministic prediction from any stale legacy manual-connect DB residue.
Production main does NOT use this command — it must use the operator-supplied
public URL at /administration/development?tab=tunnel (exactly what the
hard-refusal message tells the operator).
Hook-driven provisioning (Phase 299.4) — legacy / alternative path
Section titled “Hook-driven provisioning (Phase 299.4) — legacy / alternative path”Heavy clones are now normally created by
cinatra setup clone <name>(the CLI-owned path above) — that does NOT useEnterWorktreeand is the recommended flow. The hook path below still works for operators who drive heavy clones throughEnterWorktree; it routes through the no-slug back-compat mode and provisions the EnterWorktree-created.claude/worktrees/worktree-<name>worktree in place (no relocation, nocinatra-ai-rename).
The EnterWorktree / ExitWorktree hooks default to light branch mode (unchanged from prior). Operators opt into clone mode by placing a marker file:
<worktree>/.cinatra-clone-on-demand— per-worktree, before the hook runs.<repo-root>/.cinatra-clone-on-demand-default— repo-wide default; every new worktree gets a clone.
When the marker is present, EnterWorktree invokes cinatra setup clone
instead of cinatra setup branch. ExitWorktree detects clone mode via
cinatra clone slug-for-worktree --worktree-path <p> (registry lookup; the
canonicalisation falls back to abs-string match so a removed worktree dir
can still be found).
ExitWorktree on a clone runs cinatra clone stop only — the DB is retained.
To drop the DB run cinatra clone prune --slug <s> --yes explicitly.
Stale-clone detection (Phase 299.4)
Section titled “Stale-clone detection (Phase 299.4)”A registry slot is stale when slot.worktreePath no longer resolves to
an existing directory (operator did rm -rf the worktree, git worktree prune ran, etc.). cinatra clone list annotates stale rows with [STALE].
To bulk-prune every stale clone:
cinatra clone prune --stale --dry-run # preview targetscinatra clone prune --stale --yes # actually prune themThere is no $HOME / repo-root exclusion — Cinatra worktrees normally
live under $HOME, so excluding it would make --stale useless. The single
safety check is dir-existence.
Tailscale Funnel (Phase 299.3)
Section titled “Tailscale Funnel (Phase 299.3)”The per-clone Tailscale sidecar exposes the clone’s host-native Next.js on
a public Funnel URL. Requires TS_AUTHKEY in the operator’s shell env.
Auth-key flow
Section titled “Auth-key flow”- Generate an ephemeral preauthorised key at https://login.tailscale.com/admin/settings/keys (ephemeral so the device auto-vacates on container exit; preauthorised so no manual approval).
export TS_AUTHKEY=tskey-auth-…cinatra clone start
The key is never written to disk. The rendered compose.yml contains
the LITERAL string ${TS_AUTHKEY}; docker compose substitutes from the
spawned-process env at exec time. Stderr from docker compose up is scrubbed
of any tskey-auth-… substring before being forwarded.
The CLI flag --tailscale-authkey is rejected (both space and equals
forms). Set the env var.
MCP-side reachability check
Section titled “MCP-side reachability check”<funnel-url>/api/mcp/health is the canonical probe. Unauthenticated,
returns { status, mcpHandlerWired, serverInfo }. The route is part of
the existing /api/mcp PUBLIC_PATH_PREFIXES so Better-Auth doesn’t gate
it.
macOS host networking (Linux-only optimisation)
Section titled “macOS host networking (Linux-only optimisation)”The Tailscale sidecar uses extra_hosts: ["host.docker.internal:host- gateway"] and points at http://host.docker.internal:31NN by default
(macOS-friendly). Linux operators can pass --tailscale-host-network to
switch to network_mode: host + http://127.0.0.1:31NN (one fewer hop).
Migrating a light branch to a clone (doc-only)
Section titled “Migrating a light branch to a clone (doc-only)”Phase 299.4 explicitly leaves this manual. To “promote” a worktree from a light branch env to a clone:
# In the worktree:cinatra teardown branch --yesrm .env.localcinatra setup cloneNote: the clone is built from cinatra_seed (the scrubbed source-of-truth
snapshot). The light branch’s cinatra_<slug> schema data is intentionally
NOT carried over. If you need branch state in the clone, take a backup of
the schema first (cinatra backup create).
Phase 299 series
Section titled “Phase 299 series”- 299.1 ✅ — Seed DB + dormant deep-fork clone provisioning.
- 299.2 ✅ —
cinatra clone start|stop|status+ per-clone WayFlow container + host-native Next.js lifecycle +/api/healthendpoint. - 299.3 ✅ — Per-clone Tailscale Funnel sidecar + clone DB
mcp_server.publicBaseUrlwiring +/api/mcp/healthendpoint. - 299.4 ✅ — EnterWorktree/ExitWorktree hook clone-mode opt-in +
stale-clone detection +
prune --stalebulk cleanup. - 2026-05-15 (quick
260515-qwd) ✅ —cinatra setup clone <name>now OWNS worktree creation (../cinatra-ai-<slug>, branchcinatra-ai-<slug>, fromorigin/main) + auto-installs deps;clone pruneremoves the CLI-owned worktree and branch. Legacy no-slug / EnterWorktree-hook path preserved for back-compat. - 299.6 ✅ —
cinatra dev tunnel <start|stop|status>(dev-main Funnel verb for barepnpm dev) + dev-only exit-1 hard refusal + MR-01 source-guarded stop + D1 optimisticpublicBaseUrlwrite (decoupled from any reachability probe) + D3 deterministic collision guard.
See also
Section titled “See also”.planning/phases/299.1-seed-db-dormant-clone/{PLAN,SUMMARY,REVIEW,VERIFICATION}.md.planning/phases/299.2-clone-local-runtime/{PLAN,SUMMARY}.md.planning/phases/299.3-clone-tailscale-funnel/{PLAN,SUMMARY}.md.planning/phases/299.4-clone-hooks-polish/{PLAN,SUMMARY}.md.planning/phases/299.6-tailscale-auto-tunnel-followups/{PLAN,SUMMARY,LEARNINGS}.md