Clone-on-demand worktrees
A heavy alternative to cinatra instance branch setup that gives a git worktree its own full
Postgres database (not just a schema). Created dormant; cinatra instance clone start
brings up a per-clone WayFlow (Cinatra’s OAS Flow agent runtime) (+ optional Tailscale Funnel (a public-internet tunnel)) on demand.
CLI namespace. Local host/monorepo bootstrap commands live under
cinatra instance …(e.g.cinatra instance clone new,cinatra instance branch setup,cinatra instance tunnel). The older bare forms (cinatra clone …,cinatra setup branch,cinatra dev tunnel,cinatra teardown branch) have been removed with no back-compat alias — only thecinatra instance …forms resolve.
Two isolation paths
Section titled “Two isolation paths”cinatra instance branch setup (light) |
cinatra instance clone new (heavy) |
|
|---|---|---|
| What is isolated | A Postgres schema (cinatra_<slug>) inside the shared postgres DB |
A separate Postgres database (cinatra_clone_<slug>) |
Better Auth (the auth server library Cinatra uses) (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 instance clone new owns it → ../cinatra-ai-<slug>, branch cinatra-ai-<slug> |
| Operator action | pnpm cinatra instance branch setup (auto via hook) |
pnpm cinatra instance clone refresh-seed once, then pnpm cinatra instance clone new <name> |
| Deps install | manual pnpm install |
automatic (corepack pnpm install) on creation |
Source-schema constraint: both
clone refresh-seedandclone newrequire the sourceSUPABASE_SCHEMAto be unset or exactly"cinatra"— they throw clearly otherwise. Worktrees on a custom app schema stay onbranch setup; clone seeding currently supports only the default app schema.
Reach for clone new 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
dedicated WayFlow + public tunnel. Stick with branch setup 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 instance clone refresh-seed [--source-env <path>]
Section titled “cinatra instance 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 instance clone new [<name>] [--slug <name>] [--worktree-path <path>] [--source-env <path>] [--force]
Section titled “cinatra instance clone new [<name>] [--slug <name>] [--worktree-path <path>] [--source-env <path>] [--force]”Creates and provisions a dormant deep-fork clone. Two modes:
- CLI-owned (recommended) —
cinatra instance clone new <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). - Current-worktree mode (no name) —
cinatra instance clone newwith no slug provisions the current worktree in place (slug fromresolveRealBranchName,cinatra-ai-/worktree-prefixes stripped). This is the path theEnterWorktreehook 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 instance 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. (LANGGRAPH_CINATRA_BASE_URLis excluded from the compare because 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 instance clone prune [--worktree-path <path>] [--slug <slug>] --yes
Section titled “cinatra instance 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 hook-owned or non-CLI-owned path (e.g. a.claude/worktrees/...EnterWorktree clone) is skipped entirely — DB/slot/Redis-only behavior is kept for it. The same guard is applied per-slot byprune --stale.
cinatra instance clone list
Section titled “cinatra instance 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: clone-registry.mjs, in the @cinatra-ai/cinatra CLI package’s own repository (its
src/ directory) — the developer/operator CLI ships separately from this monorepo; see the
CLI namespace note above.
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 instance 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 by the deliberate prune-on-Redis-failure design — the DB is already dropped; make Redis reachable and re-run prune (idempotent) to release the slot.
Lifecycle commands
Section titled “Lifecycle commands”cinatra instance clone start [<slug>] [--rebuild-wayflow] [--tailscale-host-network]
Section titled “cinatra instance 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 instance clone stop [<slug>]
Section titled “cinatra instance 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 instance clone prune --slug <s> --yes.
cinatra instance clone status [<slug>]
Section titled “cinatra instance 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 instance tunnel <start|stop|status>
Section titled “cinatra instance tunnel <start|stop|status>”The bare pnpm dev MAIN-instance equivalent of the per-clone Funnel
(cinatra instance clone start). 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 instance tunnel start
Section titled “cinatra instance tunnel start”- Dev-only HARD REFUSAL. Exits with a thrown error (exit 1) unless
CINATRA_RUNTIME_MODE=development, BEFORE any Docker / Nango (the OAuth gateway brokering connector credentials) / DB side effect (dev-only boot-path convention). The message tells the operator that production main must instead use the operator-supplied public URL at/configuration/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://<your-tailnet>.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 instance tunnel stop
Section titled “cinatra instance tunnel stop”- Tears the dev-main Tailscale sidecar down.
- 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 instance tunnel status
Section titled “cinatra instance 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
deterministic prediction from any stale manual-connect DB residue.
Production main does NOT use this command — it must use the operator-supplied
public URL at /configuration/development?tab=tunnel (exactly what the
hard-refusal message tells the operator).
Hook-driven provisioning
Section titled “Hook-driven provisioning”Heavy clones are normally created by
cinatra instance clone new <name>(the CLI-owned path above) — that does NOT useEnterWorktreeand is the recommended flow. The hook path below is available for operators who drive heavy clones throughEnterWorktree; it routes through the no-slug current-worktree 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. 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 instance clone new
instead of cinatra instance branch setup. ExitWorktree detects clone mode via
cinatra instance 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 instance clone stop only — the DB is retained.
To drop the DB run cinatra instance clone prune --slug <s> --yes explicitly.
Stale-clone detection
Section titled “Stale-clone detection”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 instance clone list annotates stale rows with [STALE].
To bulk-prune every stale clone:
cinatra instance clone prune --stale --dry-run # preview targetscinatra instance 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
Section titled “Tailscale Funnel”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 instance 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
Section titled “Migrating a light branch to a clone”To move a worktree from a light branch env to a clone:
# In the worktree:cinatra instance branch teardown --yesrm .env.localcinatra instance clone newNote: 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 instance backup create).
Capability summary
Section titled “Capability summary”- Seed DB + dormant deep-fork clone provisioning.
cinatra instance clone start|stop|statusmanages the per-clone WayFlow container, host-native Next.js lifecycle, and health checks.- Per-clone Tailscale Funnel sidecar wires
mcp_server.publicBaseUrland exposes/api/mcp/health. - EnterWorktree/ExitWorktree hooks can opt into clone mode, with stale-clone detection and
prune --stalebulk cleanup. cinatra instance clone new <name>owns worktree creation (../cinatra-ai-<slug>, branchcinatra-ai-<slug>, fromorigin/main) and auto-installs deps;clone pruneremoves the CLI-owned worktree and branch. The no-slug / EnterWorktree-hook path remains available for current-worktree provisioning.cinatra instance tunnel <start|stop|status>provides the dev-main Funnel verb for barepnpm dev, with dev-only hard refusal, source-guarded stop, optimisticpublicBaseUrlwrite decoupled from reachability probes, and deterministic collision guard.
Docs content licensed under CC-BY-4.0; embedded code snippets under Apache-2.0.