Files
headquarter/openspec/changes/working-copies/spec.md
alex d567225bf7 feat: workspace backend foundation (PR-1)
- Add workspaces table migration (2026_06_01_add_workspaces)
- Create Workspace model with repo_id, user_id, branch, path, status
- Add workspace_id nullable FK to ToolInstance
- Create GitService for clone/fetch/pull/branch_exists_remotely
- Create WorkspaceManager for create/delete/sync lifecycle
- Create workspace CRUD API with 409 handling for duplicates and instances
- Wire workspace routes into FastAPI app
- 17 tests passing (8 unit + 9 integration), 1 skipped

Quality gates: ruff clean
2026-05-31 23:02:45 +02:00

8.0 KiB

Spec: Workspace-Based Tool Instances

Status

Field Value
Phase Spec
Based on Proposal
Next Design

Overview

Workspaces are persistent, writable local clones of Git repositories. Users create workspaces explicitly, then start tool instances on them. This replaces the current "mount vs clone" decision with a simple "pick a workspace" flow.

Decisions

Decision Value
Name "Workspace"
Scope Unlimited per repository
Auto-create No — explicit creation only
Delete with instances Allowed with confirmation; stops and deletes all associated tool instances
Name uniqueness Unique per project+repo; derived from project and repo names
Deleted remote branch On sync/update, detect and ask for confirmation to delete local workspace/branch

Database Schema

New Table: workspaces

CREATE TABLE workspaces (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    repo_id UUID NOT NULL REFERENCES git_repositories(id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    branch VARCHAR(255) NOT NULL DEFAULT 'main',
    path VARCHAR(2048) NOT NULL,
    status VARCHAR(16) NOT NULL DEFAULT 'ready',
    last_sync_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
    
    UNIQUE (repo_id, name)
);

CREATE INDEX idx_workspaces_repo_id ON workspaces(repo_id);
CREATE INDEX idx_workspaces_user_id ON workspaces(user_id);
CREATE INDEX idx_workspaces_status ON workspaces(status);

Updated Table: tool_instances

ALTER TABLE tool_instances
    ADD COLUMN workspace_id UUID REFERENCES workspaces(id) ON DELETE SET NULL,
    ADD COLUMN clone_mode VARCHAR(16); -- deprecated, nullable for migration

-- Drop existing clone_mode column after all instances are migrated
-- ALTER TABLE tool_instances DROP COLUMN clone_mode;

Note: tool_instances.branch remains for now but is deprecated; the canonical branch lives on the workspace.

Backend API

Workspaces API

GET   /projects/{project_id}/repositories/{repo_id}/workspaces
      → List workspaces for a repository

POST  /projects/{project_id}/repositories/{repo_id}/workspaces
      → Create a new workspace
      Body: { name: string, branch: string }

GET   /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}
      → Get workspace details

PATCH /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}
      → Update workspace (rename, change branch)
      Body: { name?: string, branch?: string }

DELETE /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}
      → Delete workspace (with ?force=true to skip confirmation)
      → Stops and deletes all associated tool instances

POST  /projects/{project_id}/repositories/{repo_id}/workspaces/{workspace_id}/sync
      → Sync workspace with remote (detect deleted branches)

Tool Instances API (Updated)

POST  /projects/{project_id}/repositories/{repo_id}/instances
      Body: { tool_type_id, workspace_id, display_name?, config_profile_id? }
      → Create instance on workspace

POST  /projects/{project_id}/repositories/{repo_id}/instances/{instance_id}/start
      → Start instance (creates workspace if mount_mode, migrates if clone_mode)

Instance Start Logic

def start_instance(instance, workspace_id=None):
    if instance.clone_mode == "clone":
        # Migrate: extract clone to workspace
        workspace = migrate_clone_to_workspace(instance)
        instance.workspace_id = workspace.id
        instance.clone_mode = None
    elif instance.workspace_id:
        # Already using a workspace
        workspace = get_workspace(instance.workspace_id)
    else:
        # Legacy mount_mode: create workspace on first start
        workspace = create_workspace_from_repo(instance.repo)
        instance.workspace_id = workspace.id
    
    # Mount workspace path into container
    mount_path = workspace.path
    # ... rest of start logic

Frontend Routes

/workspaces                    → Workspaces list page
/workspaces/new                → Create workspace flow
/workspaces/{id}               → Workspace detail page
/workspaces/{id}/tools         → Start tool on workspace

UI Components

Sidebar Navigation

Projects
  └── [project list]
Workspaces (NEW)
  └── All Workspaces
  └── [recent workspaces]
Settings

Workspaces Page

  • Grid/list of workspace cards
  • Each card shows: name, repo, branch, status, active instances count
  • Actions: Start Tool, Sync, Settings, Delete

Create Workspace Flow

  1. Repo picker: Select from existing repositories
  2. Branch picker: Select branch (default: repo's default branch)
  3. Name input: Auto-suggested as {project-name}-{repo-name}-{branch} but editable
  4. Create: Clone repo to /data/working-copies/{repo-id}/{name}/

Start Tool from Workspace

  1. Tool picker: Select tool type
  2. Config: Optional config profile
  3. Create: Instance created with workspace mounted

File System Layout

/data/working-copies/
  └── {repo-id}/
        └── {workspace-name}/
              └── .git/
              └── [repo files]

Workspace Lifecycle

Creation

  1. Validate name uniqueness per repo
  2. Clone repo: git clone --branch {branch} {remote_url} {path}
  3. Set status to ready
  4. Return workspace record

Deletion

  1. Check for running tool instances
  2. If instances exist and no ?force=true:
    • Return 409 Conflict with { instances: [...] }
    • Frontend shows confirmation dialog
  3. If confirmed:
    • Stop all associated instances
    • Delete all associated instances
    • Delete workspace directory
    • Delete workspace record

Sync

  1. Fetch from remote: git fetch origin
  2. Check if workspace branch still exists on remote
  3. If branch deleted:
    • Return 409 with { branch_deleted: true }
    • Frontend asks: "Branch '{branch}' was deleted. Delete this workspace?"
  4. If branch exists:
    • Pull changes: git pull origin {branch}
    • Update last_sync_at

Migration Strategy

Existing clone_mode Instances

def migrate_clone_to_workspace(instance):
    # Find the cloned repo inside the instance directory
    clone_path = find_clone_in_instance_dir(instance)
    
    # Create workspace
    workspace = Workspace(
        name=f"{instance.name}-migrated",
        repo_id=instance.repository_id,
        user_id=instance.owner_id,
        branch=instance.branch or "main",
        path=f"/data/working-copies/{instance.repository_id}/{instance.name}-migrated",
    )
    
    # Move clone to workspace path
    move(clone_path, workspace.path)
    
    return workspace

Existing mount_mode Instances

On first start after deployment:

  1. Create workspace from canonical repo
  2. Update instance to use workspace
  3. Remove clone_mode flag

Acceptance Criteria

  • Database migration creates workspaces table
  • Database migration adds workspace_id to tool_instances
  • API endpoints for CRUD operations on workspaces
  • Workspace creation clones repo to /data/working-copies/...
  • Workspace deletion stops and deletes associated tool instances
  • Workspace sync detects deleted branches and asks for confirmation
  • Tool instance creation accepts workspace_id instead of clone_mode
  • Tool instance start mounts workspace path into container
  • Frontend has "Workspaces" sidebar entry
  • Frontend workspaces list page
  • Frontend create workspace flow
  • Frontend start tool from workspace
  • Existing clone_mode instances migrate on restart
  • Existing mount_mode instances create workspace on restart

Quality Gates

  • Backend tests: workspace CRUD, sync, deletion with instances
  • Frontend tests: workspace list, create, start tool
  • Integration tests: instance creation with workspace
  • ruff clean
  • TypeScript compilation clean