# Spec: Workspace-Based Tool Instances ## Status | Field | Value | |---|---| | Phase | **Spec** | | Based on | [Proposal](proposal.md) | | 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` ```sql 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` ```sql 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 ```python 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 ```python 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