# RivetKit + Sandbox Provider + OpenTUI Migration Spec

Status: implemented baseline (Phase 1-4 complete, Phase 5 partial)
Date: 2026-02-08

## Locked Decisions

1. Entire rewrite is TypeScript. All Rust code will be deleted at cutover.
2. Repo stays a single monorepo, managed with `pnpm` workspaces + Turborepo.
3. `core` package is renamed to `shared`.
4. `integrations` and `providers` live inside the backend package (not top-level packages).
5. Rivet-backed state uses SQLite + Drizzle only.
6. RivetKit dependencies come from local `../rivet` builds only; no published npm packages.
7. Everything is workspace-scoped. Workspace is configurable from CLI.
8. `ControlPlaneActor` is renamed to `WorkspaceActor` (workspace coordinator).
9. Every actor key is prefixed by workspace.
10. `--workspace` is optional; commands resolve workspace via flag -> config default -> `default`.
11. RivetKit local dependency wiring is `link:`-based.
12. Keep the existing config file path (`~/.config/openhandoff/config.toml`) and evolve keys in place.
13. `.agents` and skill files are in scope for migration updates.
14. Parent orchestration actors (`workspace`, `project`, `handoff`) use command-only loops with no timeout.
15. Periodic syncing/polling runs in dedicated child actors, each with a single timeout cadence.
16. For each actor, define the main loop and exactly what data it mutates; keep single-writer ownership strict.

## Executive Summary

We will replace the existing Rust backend/CLI/TUI with TypeScript services and UIs:

- Backend: RivetKit actor runtime
- Agent orchestration: Sandbox Agent through provider adapters
- CLI: TypeScript
- TUI: TypeScript + OpenTUI
- State: SQLite + Drizzle (actor-owned writes)

The core architecture changes from "worktree-per-handoff" to "provider-selected sandbox-per-handoff." Local worktrees remain supported through a `worktree` provider.

## Breaking Changes (Intentional)

1. Rust binaries/backend removed.
2. Existing IPC replaced by new TypeScript transport.
3. Configuration schema changes for workspace selection and sandbox provider defaults.
4. Runtime model changes from global control plane to workspace coordinator actor.
5. Database schema migrates to workspace + provider + sandbox identity model.
6. Command options evolve to include workspace and provider selection.

## Monorepo and Build Tooling

Root tooling is standardized:

- `pnpm-workspace.yaml`
- `turbo.json`
- workspace scripts through `pnpm` + `turbo run ...`

Target package layout:

```text
packages/
  shared/                         # shared types, contracts, validation
  backend/
    src/
      actors/
        workspace.ts
        project.ts
        handoff.ts
        sandbox-instance.ts
        history.ts
        project-pr-sync.ts
        project-branch-sync.ts
        handoff-status-sync.ts
        keys.ts
        events.ts
        registry.ts
        index.ts
      providers/                  # provider-api + implementations
        provider-api/
        worktree/
        daytona/
      integrations/               # sandbox-agent + git/github/graphite adapters
        sandbox-agent/
        git/
        github/
        graphite/
      db/                         # drizzle schema, queries, migrations
        schema.ts
        client.ts
        migrations/
      transport/
        server.ts
        types.ts
      config/
        workspace.ts
        backend.ts
  cli/                            # hf command surface
    src/
      commands/
      client/                     # backend transport client
      workspace/                  # workspace selection resolver
  tui/                            # OpenTUI app
    src/
      app/
      views/
      state/
      client/                     # backend stream client
research/specs/
  rivetkit-opentui-migration-plan.md (this file)
```

CLI and TUI are separate packages in the same monorepo, not separate repositories.

## Actor File Map (Concrete)

Backend actor files and responsibilities:

1. `packages/backend/src/actors/workspace.ts`
- `WorkspaceActor` implementation.
- Provider profile resolution and workspace-level coordination.
- Spawns/routes to `ProjectActor` handles.

2. `packages/backend/src/actors/project.ts`
- `ProjectActor` implementation.
- Branch snapshot refresh, PR cache orchestration, stream publication.
- Routes handoff actions to `HandoffActor`.

3. `packages/backend/src/actors/handoff.ts`
- `HandoffActor` implementation.
- Handoff lifecycle, session/sandbox orchestration, post-idle automation.

4. `packages/backend/src/actors/sandbox-instance.ts`
- `SandboxInstanceActor` implementation.
- Provider sandbox lifecycle, heartbeat, reconnect/recovery.

5. `packages/backend/src/actors/history.ts`
- `HistoryActor` implementation.
- Writes workflow events to SQLite via Drizzle.

6. `packages/backend/src/actors/keys.ts`
- Workspace-prefixed actor key builders/parsers.

7. `packages/backend/src/actors/events.ts`
- Internal actor event envelopes and stream payload types.

8. `packages/backend/src/actors/registry.ts`
- RivetKit registry setup and actor registration.

9. `packages/backend/src/actors/index.ts`
- Actor exports and composition wiring.

10. `packages/backend/src/actors/project-pr-sync.ts`
- Read-only PR polling loop (single timeout cadence).
- Sends sync results back to `ProjectActor`.

11. `packages/backend/src/actors/project-branch-sync.ts`
- Read-only branch snapshot polling loop (single timeout cadence).
- Sends sync results back to `ProjectActor`.

12. `packages/backend/src/actors/handoff-status-sync.ts`
- Read-only session/sandbox status polling loop (single timeout cadence).
- Sends status updates back to `HandoffActor`.

## RivetKit Source Policy (Local Only)

Do not use published RivetKit packages.

1. Build RivetKit from local source:
```bash
cd ../rivet
pnpm build -F rivetkit
```
2. Consume via local `link:` dependencies to built artifacts.
3. Keep dependency wiring deterministic and documented in repo scripts.

## Workspace Model

Every command executes against a resolved workspace context.

Workspace selection:

1. CLI flag: `--workspace <name>`
2. Config default workspace
3. Fallback to `default`

Workspace controls:

1. provider profile defaults
2. sandbox policy
3. repo membership / resolution
4. actor namespaces and database partitioning

## New Actor Implementation Overview

RivetKit registry actor keys are workspace-prefixed:

1. `WorkspaceActor` (workspace coordinator)
- Key: `["ws", workspaceId]`
- Owns workspace config/runtime coordination, provider registry, workspace health.
- Resolves provider defaults and workspace-level policies.

2. `ProjectActor`
- Key: `["ws", workspaceId, "project", repoId]`
- Owns repo snapshot cache and PR cache refresh orchestration.
- Routes branch/handoff commands to handoff actors.
- Streams project updates to CLI/TUI subscribers.

3. `HandoffActor`
- Key: `["ws", workspaceId, "project", repoId, "handoff", handoffId]`
- Owns handoff metadata/runtime state.
- Creates/resumes sandbox + session through provider adapter.
- Handles attach/push/sync/merge/archive/kill and post-idle automation.

4. `SandboxInstanceActor` (optional but recommended)
- Key: `["ws", workspaceId, "provider", providerId, "sandbox", sandboxId]`
- Owns sandbox lifecycle, heartbeat, endpoint readiness, recovery.

5. `HistoryActor`
- Key: `["ws", workspaceId, "project", repoId, "history"]`
- Owns `events` writes and workflow timeline completeness.

6. `ProjectPrSyncActor` (child poller)
- Key: `["ws", workspaceId, "project", repoId, "pr-sync"]`
- Polls PR state on interval and emits results to `ProjectActor`.
- Does not write DB directly.

7. `ProjectBranchSyncActor` (child poller)
- Key: `["ws", workspaceId, "project", repoId, "branch-sync"]`
- Polls branch/worktree state on interval and emits results to `ProjectActor`.
- Does not write DB directly.

8. `HandoffStatusSyncActor` (child poller)
- Key: `["ws", workspaceId, "project", repoId, "handoff", handoffId, "status-sync"]`
- Polls agent/session/sandbox health on interval and emits results to `HandoffActor`.
- Does not write DB directly.

Ownership rule: each table/row has one actor writer.

## Single-Writer Mutation Map

Always define actor run-loop + mutated state together:

1. `WorkspaceActor`
- Mutates: `workspaces`, `workspace_provider_profiles`.

2. `ProjectActor`
- Mutates: `repos`, `branches`, `pr_cache` (applies child poller results).

3. `HandoffActor`
- Mutates: `handoffs`, `handoff_runtime` (applies child poller results).

4. `SandboxInstanceActor`
- Mutates: `sandbox_instances`.

5. `HistoryActor`
- Mutates: `events`.

6. Child sync actors (`project-pr-sync`, `project-branch-sync`, `handoff-status-sync`)
- Mutates: none (read-only pollers; publish result messages only).

## Run Loop Patterns (Required)

Parent orchestration actors: no timeout, command-only queue loops.

### `WorkspaceActor` (no timeout)

```ts
run: async (c) => {
  while (true) {
    const msg = await c.queue.next("workspace.command");
    await handleWorkspaceCommand(c, msg); // writes workspace-owned tables only
  }
};
```

### `ProjectActor` (no timeout)

```ts
run: async (c) => {
  while (true) {
    const msg = await c.queue.next("project.command");
    await handleProjectCommand(c, msg); // includes applying sync results to branches/pr_cache
  }
};
```

### `HandoffActor` (no timeout)

```ts
run: async (c) => {
  while (true) {
    const msg = await c.queue.next("handoff.command");
    await handleHandoffCommand(c, msg); // includes applying status results to handoff_runtime
  }
};
```

### `SandboxInstanceActor` (no timeout)

```ts
run: async (c) => {
  while (true) {
    const msg = await c.queue.next("sandbox_instance.command");
    await handleSandboxInstanceCommand(c, msg); // sandbox_instances table only
  }
};
```

### `HistoryActor` (no timeout)

```ts
run: async (c) => {
  while (true) {
    const msg = await c.queue.next("history.command");
    await persistEvent(c, msg); // events table only
  }
};
```

Child sync actors: one timeout each, one cadence each.

### `ProjectPrSyncActor` (single timeout cadence)

```ts
run: async (c) => {
  const intervalMs = 30_000;
  while (true) {
    const msg = await c.queue.next("project.pr_sync.command", { timeout: intervalMs });
    if (!msg) {
      const result = await pollPrState();
      await sendToProject({ name: "project.pr_sync.result", result });
      continue;
    }
    await handlePrSyncControl(c, msg); // force/stop/update-interval
  }
};
```

### `ProjectBranchSyncActor` (single timeout cadence)

```ts
run: async (c) => {
  const intervalMs = 5_000;
  while (true) {
    const msg = await c.queue.next("project.branch_sync.command", { timeout: intervalMs });
    if (!msg) {
      const result = await pollBranchState();
      await sendToProject({ name: "project.branch_sync.result", result });
      continue;
    }
    await handleBranchSyncControl(c, msg);
  }
};
```

### `HandoffStatusSyncActor` (single timeout cadence)

```ts
run: async (c) => {
  const intervalMs = 2_000;
  while (true) {
    const msg = await c.queue.next("handoff.status_sync.command", { timeout: intervalMs });
    if (!msg) {
      const result = await pollSessionAndSandboxStatus();
      await sendToHandoff({ name: "handoff.status_sync.result", result });
      continue;
    }
    await handleStatusSyncControl(c, msg);
  }
};
```

## Sandbox Provider Interface

Provider contract lives under `packages/backend/src/providers/provider-api` and is consumed by workspace/project/handoff actors.

```ts
interface SandboxProvider {
  id(): string;
  capabilities(): ProviderCapabilities;
  validateConfig(input: unknown): Promise<ValidatedConfig>;

  createSandbox(req: CreateSandboxRequest): Promise<SandboxHandle>;
  resumeSandbox(req: ResumeSandboxRequest): Promise<SandboxHandle>;
  destroySandbox(req: DestroySandboxRequest): Promise<void>;

  ensureSandboxAgent(req: EnsureAgentRequest): Promise<AgentEndpoint>;
  health(req: SandboxHealthRequest): Promise<SandboxHealth>;
  attachTarget(req: AttachTargetRequest): Promise<AttachTarget>;
}
```

Initial providers:

1. `worktree`
- Local git worktree-backed sandbox.
- Sandbox Agent local/shared endpoint.
- Preserves tmux + `cd` ergonomics.

2. `daytona`
- Remote sandbox lifecycle via Daytona.
- Boots/ensures Sandbox Agent inside sandbox.
- Returns endpoint/token for session operations.

## Command Surface (Workspace + Provider Aware)

1. `hf create ... --workspace <ws> --provider <worktree|daytona>`
2. `hf switch --workspace <ws> [target]`
3. `hf attach --workspace <ws> [handoff]`
4. `hf list --workspace <ws>`
5. `hf kill|archive|merge|push|sync --workspace <ws> ...`
6. `hf workspace use <ws>` to set default workspace

List/TUI include provider and sandbox health metadata.

`--workspace` remains optional; omitted values use the standard resolution order.

## Data Model v2 (SQLite + Drizzle)

All persistent state is SQLite via Drizzle schema + migrations.

Tables (workspace-scoped):

1. `workspaces`
2. `workspace_provider_profiles`
3. `repos` (`workspace_id`, `repo_id`, ...)
4. `branches` (`workspace_id`, `repo_id`, ...)
5. `handoffs` (`workspace_id`, `handoff_id`, `provider_id`, ...)
6. `handoff_runtime` (`workspace_id`, `handoff_id`, `sandbox_id`, `session_id`, ...)
7. `sandbox_instances` (`workspace_id`, `provider_id`, `sandbox_id`, ...)
8. `pr_cache` (`workspace_id`, `repo_id`, ...)
9. `events` (`workspace_id`, `repo_id`, ...)

Migration approach: one-way migration from existing schema during TS backend bootstrap.

## Transport and Runtime

1. TypeScript backend exposes local control API (socket or localhost HTTP).
2. CLI/TUI are thin clients; all mutations go through backend actors.
3. OpenTUI subscribes to project streams from workspace-scoped project actors.
4. Workspace is required context on all backend mutation requests.

CLI/TUI are responsible for resolving workspace context before calling backend mutations.

## CLI + TUI Packaging

CLI and TUI ship from one package:

1. `packages/cli`
- command-oriented UX (`hf create`, `hf push`, scripting, JSON output)
- interactive OpenTUI mode via `hf tui`
- shared client/runtime wiring in one distributable

The package still calls the same backend API and shares contracts from `packages/shared`.

## Implementation Phases

## Phase 0: Contracts and Workspace Spec

1. Freeze workspace model, provider contract, and actor ownership map.
2. Freeze command flags for workspace + provider selection.
3. Define Drizzle schema draft and migration plan.

Exit criteria:
- Approved architecture RFC.

## Phase 1: TypeScript Monorepo Bootstrap

1. Add `pnpm` workspace + Turborepo pipeline.
2. Create `shared`, `backend`, and `cli` packages (with TUI integrated into CLI).
3. Add strict TypeScript config and CI checks.

Exit criteria:
- `pnpm -w typecheck` and `turbo run build` pass.

## Phase 2: RivetKit + Drizzle Foundations

1. Wire local RivetKit dependency from `../rivet`.
2. Add SQLite + Drizzle migrations and query layer.
3. Implement actor registry with workspace-prefixed keys.

Exit criteria:
- Backend boot + workspace actor health checks pass.

## Phase 3: Provider Layer in Backend

1. Implement provider API inside backend package.
2. Implement `worktree` provider end-to-end.
3. Integrate sandbox-agent session lifecycle through provider.

Exit criteria:
- `create/list/switch/attach/push/sync/kill` pass on worktree provider.

## Phase 4: Workspace/Handoff Lifecycle

1. Implement workspace coordinator flows.
2. Implement HandoffActor full lifecycle + post-idle automation.
3. Implement history events and PR/CI/review change tracking.

Exit criteria:
- history/event completeness with parity checks.

## Phase 5: Daytona Provider

1. Implement Daytona sandbox lifecycle adapter.
2. Ensure sandbox-agent boot and reconnection behavior.
3. Validate attach/switch/kill flows for remote sandboxes.

Exit criteria:
- e2e pass on daytona provider.

## Phase 6: OpenTUI Rewrite

1. Build interactive list/switch UI in OpenTUI.
2. Implement key actions (attach/open PR/archive/merge/sync).
3. Add workspace switcher UX and provider/sandbox indicators.

Exit criteria:
- TUI parity and responsive streaming updates.

## Phase 7: Cutover + Rust Deletion

1. Migrate existing DB to v2.
2. Replace runtime entrypoints with TS CLI/backend/TUI.
3. Delete Rust code, Cargo files, Rust scripts.
4. Update docs and `skills/SKILL.md`.

Exit criteria:
- no Rust code remains, fresh install + upgrade validated.

## Testing Strategy

1. Unit tests
- actor actions and ownership rules
- provider adapters
- event emission correctness
- drizzle query/migration tests

2. Integration tests
- backend + sqlite + provider fakes
- workspace isolation boundaries
- session recovery and restart handling

3. E2E tests
- worktree provider in local test repo
- daytona provider in controlled env
- OpenTUI interactive flows

4. Reliability tests
- sandbox-agent restarts
- transient provider failures
- backend restart with in-flight handoffs

## Open Questions To Resolve Before Implementation

1. Daytona production adapter parity:
- Current `daytona` provider in this repo is intentionally a fallback adapter so local development remains testable without a Daytona backend.
- Final deployment integration should replace placeholder lifecycle calls with Daytona API operations (create/destroy/health/auth/session boot inside sandbox).
