# 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
