Skip to content

feat(gastown): Fork Town — create a new town from an existing town's settings and bead history #2368

@jrf0110

Description

@jrf0110

Summary

Add the ability to "fork" an existing town, creating a new town that inherits the source town's configuration and bead history. This is useful for:

  • Spinning up a new town with proven settings from a well-tuned existing town
  • Preserving historical context when changing ownership (personal → org, cross-org)
  • Creating team templates — configure one town perfectly, fork it for each new project

What Gets Copied

Settings (from TownConfig KV)

  • Model config (default_model, role_models, small_model)
  • Merge strategy, refinery config (gates, auto_merge, review_mode)
  • Custom instructions, env vars, alarm intervals, convoy settings
  • Git author name/email (optional)

Structural Data (SQL tables)

Table Copy Strategy
rigs Copy with new UUIDs, same git_url and default_branch
beads Terminal only (closed/failed). Skip agent, message, and active beads. Clear assignee_agent_bead_id, reset dispatch_attempts
bead_events Copy events for copied beads only
bead_dependencies Copy edges where both beads exist in the copied set
review_metadata Copy for closed/failed MR beads. Clear auto_merge_ready_since, last_feedback_check_at
escalation_metadata Copy for copied escalation beads
convoy_metadata Copy only fully landed convoys (landed_at IS NOT NULL)

What Gets Skipped

  • agent_metadata — runtime state, agents created fresh on demand
  • agent_nudges — ephemeral runtime messages
  • town_events — reconciler queue, ephemeral
  • Active beads (open/in_progress/in_review)
  • Active convoys (not yet landed)
  • Message beads (inter-agent mail)
  • All container/drain/dashboard state

What Gets Minted Fresh

  • Town UUID, rig UUIDs
  • kilocode_token (per new owner via mintKilocodeToken)
  • Git tokens (via refreshGitCredentials for new owner)
  • Ownership fields (owner_user_id, organization_id, owner_type, etc.)
  • Container (new TownContainerDO instance, keyed by new town ID)

Architecture

Export/Import via Worker Layer

forkTown(sourceTownId, targetOwnerType, targetOwnerId, newTownName):
  1. Verify caller has read access to source town
  2. Create town record in owner DO (GastownUserDO or GastownOrgDO)
  3. sourceTownDO.exportForFork() → serialized JSON payload
  4. Build rig ID mapping: oldRigId → newRigId
  5. Create new TownDO, call setTownId()
  6. newTownDO.importFork(payload, rigIdMapping):
     - INSERT beads (remapping rig_id via mapping)
     - INSERT bead_events (for copied beads only)
     - INSERT bead_dependencies (for copied beads only)
     - INSERT review/escalation/convoy_metadata
     - INSERT rigs (with new IDs)
  7. Set TownConfig: copy settings, set new ownership, mint fresh credentials
  8. For each rig: configureRig() + refreshGitCredentials()
  9. Register rigs in owner DO

Key Design Decisions

  • Bead IDs are preserved — each DO has its own SQLite, so no cross-DO collision. Preserving IDs keeps parent_bead_id and bead_dependencies edges intact without remapping.
  • Rig IDs must change — they're registered in the owner DO's user_rigs table, so new UUIDs prevent collisions. All rig_id references in copied beads are remapped.
  • Payload size — a 1000-bead town with 5000 events is ~2MB JSON, well within DO memory limits. For very large towns (>10K beads), could add R2-backed streaming export as a follow-up.

Cross-Ownership Support

Source Target Notes
Personal → Personal New owner or same user
Personal → Org Forker must be org member
Org → Personal Any org member can fork to personal
Org → Org Cross-org, forker needs target org membership

Edge Cases

  • Partial convoys — only copy fully landed convoys (landed_at IS NOT NULL). Active convoys reference in-progress branches and agents that won't exist in the fork.
  • FK consistencybead_dependencies has FK constraints. Only copy edges where both bead_id and depends_on_bead_id are in the copied set.
  • Orphaned agent referencesbead_events.agent_id, beads.created_by, and metadata.source_agent_id will reference agent IDs that don't exist in the fork. This is fine for read-only history display.
  • Git access — the new owner must have the GitHub App installed on the same repos, or rigs won't function until credentials are configured.
  • Race conditions — export is a snapshot. Filter to terminal beads (immutable per terminal state guard in beads.ts:282) to avoid inconsistency.
  • env_vars — copied as-is, but may contain secrets (npm tokens, API keys). The fork UI should warn the user.

Implementation Surface

New Code

  • TownDO.exportForFork() — RPC method, queries all copyable tables, returns ForkExportPayload
  • TownDO.importFork(payload, rigIdMapping) — RPC method, bulk inserts with remapping
  • forkTown / forkOrgTown tRPC mutations — orchestrate the full flow
  • UI: "Fork Town" button in town settings or town list

Existing Code Touched

File Why
src/dos/Town.do.ts Add exportForFork() and importFork() RPC methods
src/trpc/router.ts Add forkTown / forkOrgTown mutations
src/dos/GastownUser.do.ts Reuse createTown() + createRig()
src/dos/GastownOrg.do.ts Reuse createTown() + createRig()
src/dos/town/beads.ts Add bulk insert helper for imported beads
src/dos/town/rigs.ts Add bulk insert helper for imported rigs
Town settings UI Add "Fork Town" action

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Post-launchenhancementNew feature or requestgt:coreReconciler, state machine, bead lifecycle, convoy flowgt:uiDashboard, settings, terminal, drawers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions